From 134371dc1766698686babb90fe7fc358a71bede2 Mon Sep 17 00:00:00 2001 From: M Pacer Date: Fri, 1 Sep 2017 15:00:57 -0700 Subject: [PATCH 001/671] add feature to improve docs by having links to prs --- docs/source/changelog.rst | 104 ++++++++++++------------- docs/source/conf.py | 11 ++- docs/sphinxext/github.py | 156 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 217 insertions(+), 54 deletions(-) create mode 100644 docs/sphinxext/github.py diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst index c66201d33..3746fe3ab 100644 --- a/docs/source/changelog.rst +++ b/docs/source/changelog.rst @@ -46,30 +46,30 @@ The following traitlets remove elements of different kinds: Comprehensive notes ~~~~~~~~~~~~~~~~~~~ -- new: configurable ``browser`` in ServePostProcessor #618 -- new: ``--clear-output`` command line flag to clear output in-place #619 -- new: remove elements based on tags with ``TagRemovePreprocessor``. #640, #643 -- new: CellExecutionError can now be imported from ``nbconvert.preprocessors`` #656 -- new: slides now can enable scrolling and custom transitions #600 +- new: configurable ``browser`` in ServePostProcessor :ghpull:`618` +- new: ``--clear-output`` command line flag to clear output in-place :ghpull:`619` +- new: remove elements based on tags with ``TagRemovePreprocessor``. :ghpull:`640`, :ghpull:`643` +- new: CellExecutionError can now be imported from ``nbconvert.preprocessors`` :ghpull:`656` +- new: slides now can enable scrolling and custom transitions :ghpull:`600` - docs: Release instructions for nbviewer-deploy -- docs: improved instructions for handling errors using the ``ExecutePreprocessor`` #656 +- docs: improved instructions for handling errors using the ``ExecutePreprocessor`` :ghpull:`656` -- tests: better height/width metadata testing for images in rst & html #601 #602 -- tests: normalise base64 output data to avoid false positives #650 -- tests: normalise ipython traceback messages to handle old and new style #631 +- tests: better height/width metadata testing for images in rst & html :ghpull:`601` :ghpull:`602` +- tests: normalise base64 output data to avoid false positives :ghpull:`650` +- tests: normalise ipython traceback messages to handle old and new style :ghpull:`631` -- bug: mathjax obeys ``\\(\\)`` & ``\\[\\]`` (both nbconvert & pandoc) #609 #617 -- bug: specify default templates using extensions #639 -- bug: fix pandoc version number #638 -- bug: require recent mistune version #630 -- bug: catch errors from IPython ``execute_reply`` and ``error`` messages #642 +- bug: mathjax obeys ``\\(\\)`` & ``\\[\\]`` (both nbconvert & pandoc) :ghpull:`609` :ghpull:`617` +- bug: specify default templates using extensions :ghpull:`639` +- bug: fix pandoc version number :ghpull:`638` +- bug: require recent mistune version :ghpull:`630` +- bug: catch errors from IPython ``execute_reply`` and ``error`` messages :ghpull:`642` -- nose completely removed & dependency dropped #595 #660 -- mathjax processing in mistune now only uses inline grammar #611 -- removeRegex now enabled by default on all TemplateExporters, does not remove cells with outputs #616 -- validate notebook after applying each preprocessor (allowing additional attributes) #645 -- changed COPYING.md to LICENSE for more standard licensing that GitHub knows how to read #654 +- nose completely removed & dependency dropped :ghpull:`595` :ghpull:`660` +- mathjax processing in mistune now only uses inline grammar :ghpull:`611` +- removeRegex now enabled by default on all TemplateExporters, does not remove cells with outputs :ghpull:`616` +- validate notebook after applying each preprocessor (allowing additional attributes) :ghpull:`645` +- changed COPYING.md to LICENSE for more standard licensing that GitHub knows how to read :ghpull:`654` 5.2.1 ----- @@ -117,32 +117,32 @@ that wish to have a language specific exporter can now surface that directly. Comprehensive notes ~~~~~~~~~~~~~~~~~~~ -- new: configurable ExecutePreprocessor.startup_timeout configurable #583 -- new: RemoveCell preprocessor based on cell content (defaults to empty cell) #575 -- new: function for executing notebooks: `executenb` #573 -- new: global filtering to remove inputs, outputs, markdown cells (&c.), this works on all templates #554 -- new: script exporter entrypoint #531 -- new: configurable anchor link text (previously ¶) `HTMLExporter.anchor_link_text` #522 - -- new: configurable values for slides exporter #542 #558 - -- improved releases (how-to documentation, version-number generation and checking) #593 -- doc improvements #593 #580 #565 #554 -- language information from cell magics (for highlighting) is now included in more formats #586 -- mathjax upgrades and cdn fixes #584 #567 -- better CI #571 #540 -- better traceback behaviour when execution errs #521 -- deprecated nose test features removed #519 - -- bug fixed: we now respect width and height metadata on jpeg and png mimetype outputs #588 -- bug fixed: now we respect the `resolve_references` filter in `report.tplx` #577 -- bug fixed: output metadata now is removed by ClearOutputPreprocessor #569 -- bug fixed: display id respected in execute preproessor #563 -- bug fixed: dynamic defaults for optional jupyter_client import #559 -- bug fixed: don't self-close non-void HTML tags #548 -- buf fixed: upgrade jupyter_client dependency to 4.2 #539 -- bug fixed: LaTeX output through md→LaTeX conversion shouldn't be touched #535 -- bug fixed: now we escape `<` inside math formulas when converting to html #514 +- new: configurable ExecutePreprocessor.startup_timeout configurable :ghpull:`583` +- new: RemoveCell preprocessor based on cell content (defaults to empty cell) :ghpull:`575` +- new: function for executing notebooks: `executenb` :ghpull:`573` +- new: global filtering to remove inputs, outputs, markdown cells (&c.), this works on all templates :ghpull:`554` +- new: script exporter entrypoint :ghpull:`531` +- new: configurable anchor link text (previously ¶) `HTMLExporter.anchor_link_text` :ghpull:`522` + +- new: configurable values for slides exporter :ghpull:`542` :ghpull:`558` + +- improved releases (how-to documentation, version-number generation and checking) :ghpull:`593` +- doc improvements :ghpull:`593` :ghpull:`580` :ghpull:`565` :ghpull:`554` +- language information from cell magics (for highlighting) is now included in more formats :ghpull:`586` +- mathjax upgrades and cdn fixes :ghpull:`584` :ghpull:`567` +- better CI :ghpull:`571` :ghpull:`540` +- better traceback behaviour when execution errs :ghpull:`521` +- deprecated nose test features removed :ghpull:`519` + +- bug fixed: we now respect width and height metadata on jpeg and png mimetype outputs :ghpull:`588` +- bug fixed: now we respect the `resolve_references` filter in `report.tplx` :ghpull:`577` +- bug fixed: output metadata now is removed by ClearOutputPreprocessor :ghpull:`569` +- bug fixed: display id respected in execute preproessor :ghpull:`563` +- bug fixed: dynamic defaults for optional jupyter_client import :ghpull:`559` +- bug fixed: don't self-close non-void HTML tags :ghpull:`548` +- buf fixed: upgrade jupyter_client dependency to 4.2 :ghpull:`539` +- bug fixed: LaTeX output through md→LaTeX conversion shouldn't be touched :ghpull:`535` +- bug fixed: now we escape `<` inside math formulas when converting to html :ghpull:`514` Credits ~~~~~~~ @@ -200,13 +200,13 @@ alphabetical order): `5.1 on GitHub `__ -- improved CSS (specifically tables, in line with notebook) #498 -- improve in-memory templates handling #491 -- test improvements #516 #509 #505 -- new configuration option: IOPub timeout #513 -- doc improvements #489 #500 #493 #506 -- newly customizable: output prompt #500 -- more python2/3 compatibile unicode handling #502 +- improved CSS (specifically tables, in line with notebook) :ghpull:`498` +- improve in-memory templates handling :ghpull:`491` +- test improvements :ghpull:`516` :ghpull:`509` :ghpull:`505` +- new configuration option: IOPub timeout :ghpull:`513` +- doc improvements :ghpull:`489` :ghpull:`500` :ghpull:`493` :ghpull:`506` +- newly customizable: output prompt :ghpull:`500` +- more python2/3 compatibile unicode handling :ghpull:`502` 5.0 --- diff --git a/docs/source/conf.py b/docs/source/conf.py index 7c1cb86a7..72246b1ba 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -14,17 +14,19 @@ # serve to show the default. import os +import sys # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. -#sys.path.insert(0, os.path.abspath('.')) +# sys.path.insert(0, os.path.abspath('.')) +sys.path.insert(0, os.path.abspath(os.path.join('..', 'sphinxext'))) if os.environ.get('READTHEDOCS', ''): # RTD doesn't use the repo's Makefile to build docs. We run # autogen_config.py to create the config docs (i.e. Configuration Options # page). - import sys, subprocess + import subprocess # subprocess.run([sys.executable,'-m','pip','install','-e','../../.']) @@ -45,8 +47,10 @@ 'sphinx.ext.napoleon', 'nbsphinx', 'IPython.sphinxext.ipython_console_highlighting', + 'github', ] + # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] @@ -68,6 +72,9 @@ copyright = '2015-%s, Jupyter Development Team' % year author = 'Jupyter Development Team' +# ghissue config +github_project_url = "https://github.com/jupyter/nbconvert" + # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. diff --git a/docs/sphinxext/github.py b/docs/sphinxext/github.py new file mode 100644 index 000000000..3de499247 --- /dev/null +++ b/docs/sphinxext/github.py @@ -0,0 +1,156 @@ +"""Define text roles for GitHub + +* ghissue - Issue +* ghpull - Pull Request +* ghuser - User + +Adapted from bitbucket example here: +https://github.com/ipython/ipython/blob/master/docs/sphinxext/github.py + +Authors +------- + +* Doug Hellmann +* M Pacer +* Min RK +""" +# +# Original Copyright (c) 2010 Doug Hellmann. All rights reserved. +# + +from docutils import nodes, utils +from docutils.parsers.rst.roles import set_classes + +def make_link_node(rawtext, app, type, slug, options): + """Create a link to a github resource. + + :param rawtext: Text being replaced with link node. + :param app: Sphinx application context + :param type: Link type (issues, changeset, etc.) + :param slug: ID of the thing to link to + :param options: Options dictionary passed to role func. + """ + + try: + base = app.config.github_project_url + if not base: + raise AttributeError + if not base.endswith('/'): + base += '/' + except AttributeError as err: + raise ValueError('github_project_url configuration value is not set (%s)' % str(err)) + + ref = base + type + '/' + slug + '/' + set_classes(options) + prefix = "#" + node = nodes.reference(rawtext, prefix + utils.unescape(slug), refuri=ref, + **options) + return node + +def ghissue_role(name, rawtext, text, lineno, inliner, options={}, content=[]): + """Link to a GitHub issue. + + Returns 2 part tuple containing list of nodes to insert into the + document and a list of system messages. Both are allowed to be + empty. + + :param name: The role name used in the document. + :param rawtext: The entire markup snippet, with role. + :param text: The text marked with the role. + :param lineno: The line number where rawtext appears in the input. + :param inliner: The inliner instance that called us. + :param options: Directive options for customization. + :param content: The directive content for customization. + """ + + try: + issue_num = int(text) + if issue_num <= 0: + raise ValueError + except ValueError: + msg = inliner.reporter.error( + 'GitHub issue number must be a number greater than or equal to 1; ' + '"%s" is invalid.' % text, line=lineno) + prb = inliner.problematic(rawtext, rawtext, msg) + return [prb], [msg] + app = inliner.document.settings.env.app + #app.info('issue %r' % text) + if 'pull' in name.lower(): + category = 'pull' + elif 'issue' in name.lower(): + category = 'issues' + else: + msg = inliner.reporter.error( + 'GitHub roles include "ghpull" and "ghissue", ' + '"%s" is invalid.' % name, line=lineno) + prb = inliner.problematic(rawtext, rawtext, msg) + return [prb], [msg] + node = make_link_node(rawtext, app, category, str(issue_num), options) + return [node], [] + +def ghuser_role(name, rawtext, text, lineno, inliner, options={}, content=[]): + """Link to a GitHub user. + + Returns 2 part tuple containing list of nodes to insert into the + document and a list of system messages. Both are allowed to be + empty. + + :param name: The role name used in the document. + :param rawtext: The entire markup snippet, with role. + :param text: The text marked with the role. + :param lineno: The line number where rawtext appears in the input. + :param inliner: The inliner instance that called us. + :param options: Directive options for customization. + :param content: The directive content for customization. + """ + app = inliner.document.settings.env.app + # app.info('user link %r' % text) + ref = 'https://www.github.com/' + text + node = nodes.reference(rawtext, text, refuri=ref, **options) + return [node], [] + +def ghcommit_role(name, rawtext, text, lineno, inliner, options={}, content=[]): + """Link to a GitHub commit. + + Returns 2 part tuple containing list of nodes to insert into the + document and a list of system messages. Both are allowed to be + empty. + + :param name: The role name used in the document. + :param rawtext: The entire markup snippet, with role. + :param text: The text marked with the role. + :param lineno: The line number where rawtext appears in the input. + :param inliner: The inliner instance that called us. + :param options: Directive options for customization. + :param content: The directive content for customization. + """ + app = inliner.document.settings.env.app + #app.info('user link %r' % text) + try: + base = app.config.github_project_url + if not base: + raise AttributeError + if not base.endswith('/'): + base += '/' + except AttributeError as err: + raise ValueError('github_project_url configuration value is not set (%s)' % str(err)) + + ref = base + text + node = nodes.reference(rawtext, text[:6], refuri=ref, **options) + return [node], [] + + +def setup(app): + """Install the plugin. + + :param app: Sphinx application context. + """ + app.info('Initializing GitHub plugin') + app.add_role('ghissue', ghissue_role) + app.add_role('ghpull', ghissue_role) + app.add_role('ghuser', ghuser_role) + app.add_role('ghcommit', ghcommit_role) + app.add_config_value('github_project_url', None, 'env') + + metadata = {'parallel_read_safe': True, 'parallel_write_safe': True} + return metadata From 39e1dd65ed2a8afab29ff3c725f833de76b1e580 Mon Sep 17 00:00:00 2001 From: M Pacer Date: Fri, 1 Sep 2017 18:27:34 -0700 Subject: [PATCH 002/671] fix language describing the high level release content --- docs/source/changelog.rst | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst index 3746fe3ab..85a989468 100644 --- a/docs/source/changelog.rst +++ b/docs/source/changelog.rst @@ -14,7 +14,9 @@ Major features Tag Based Element Filtering +++++++++++++++++++++++++++ -For removing individual elements we need a way to signal that, with this release we introduce the use of tags for that purpose. +For removing individual elements from notebooks, we need a way to signal to +nbconvert that the elements should be removed. With this release, we introduce +the use of tags for that purpose. Tags are user-defined strings attached to cells or outputs. They are stored in cell or output metadata. For more on tags see the `nbformat docs on cell From 7cfab4b3b4c803e96e397fb095a8b19740d65ff6 Mon Sep 17 00:00:00 2001 From: M Pacer Date: Tue, 5 Sep 2017 14:53:21 -0700 Subject: [PATCH 003/671] return version number to 5.3.2dev --- nbconvert/_version.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nbconvert/_version.py b/nbconvert/_version.py index a61b0c312..466165295 100644 --- a/nbconvert/_version.py +++ b/nbconvert/_version.py @@ -1,6 +1,6 @@ -version_info = (5, 3, 1) +version_info = (5, 3, 2) pre_info = '' -dev_info = '' +dev_info = 'dev' def create_valid_version(release_info, epoch=None, pre_input='', dev_input=''): ''' From ca2b4a9fae4de4174a5109f3441c1094673b245e Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Wed, 6 Sep 2017 09:14:17 -0700 Subject: [PATCH 004/671] Explicitetly exclude or include all files in Manifest. And test for it. Finish up #667 --- .travis.yml | 2 ++ MANIFEST.in | 6 ++++++ setup.cfg | 4 ++++ 3 files changed, 12 insertions(+) diff --git a/.travis.yml b/.travis.yml index e12860d77..0eaee345f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -32,8 +32,10 @@ install: - pip install --upgrade setuptools pip - pip install -f travis-wheels/wheelhouse . codecov coverage - pip install nbconvert[execute,serve,test] + - pip install check-manifest - python -m ipykernel.kernelspec --user script: + - check-manifest # cd so we test the install, not the repo - cd `mktemp -d` - py.test --cov nbconvert -v --pyargs nbconvert diff --git a/MANIFEST.in b/MANIFEST.in index 7944edc65..fac2df1eb 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,10 +1,16 @@ include LICENSE include CONTRIBUTING.md include README.md +include .mailmap +include MANIFEST.in +include nbconvert/templates/skeleton/Makefile +include nbconvert/tests/README.md # Documentation graft docs exclude docs/\#* +exclude readthedocs.yml +exclude codecov.yml # Examples graft examples diff --git a/setup.cfg b/setup.cfg index 3c6e79cf3..792734b72 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,2 +1,6 @@ [bdist_wheel] universal=1 + +[check-manifest] +ignore = + nbconvert/resources/style.min.css From 5c0279a12ee00ecb04bd36ad72061a1160e9cb6d Mon Sep 17 00:00:00 2001 From: M Pacer Date: Fri, 8 Sep 2017 00:16:14 -0700 Subject: [PATCH 005/671] change logic to grab and place as title, unicode escaping isn't working though --- nbconvert/templates/latex/base.tplx | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/nbconvert/templates/latex/base.tplx b/nbconvert/templates/latex/base.tplx index 5263a70bb..3c4e39475 100644 --- a/nbconvert/templates/latex/base.tplx +++ b/nbconvert/templates/latex/base.tplx @@ -139,7 +139,14 @@ This template does not define a docclass, the inheriting class must define this. \def\gt{>} \def\lt{<} % Document parameters - ((* block title *))\title{((( resources.metadata.name | ascii_only | escape_latex )))}((* endblock title *)) + % Document title + ((* block title -*)) + ((*- if "title" in nb.metadata *)) + \title{((( nb.metadata.get("title", "") | ascii_only | escape_latex )))} + ((*- else *)) + \title{((( resources.metadata.name | ascii_only | escape_latex )))} + ((*- endif -*)) + ((*- endblock title *)) ((* block date *))((* endblock date *)) ((* block author *))((* endblock author *)) ((* endblock definitions *)) From 5ce4ca7d12d73a9199550a35b39f73ce13a53a95 Mon Sep 17 00:00:00 2001 From: M Pacer Date: Wed, 13 Sep 2017 14:55:11 -0700 Subject: [PATCH 006/671] handle raw_template via attribute --- nbconvert/exporters/templateexporter.py | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/nbconvert/exporters/templateexporter.py b/nbconvert/exporters/templateexporter.py index 019850cb3..ddafc73f8 100644 --- a/nbconvert/exporters/templateexporter.py +++ b/nbconvert/exporters/templateexporter.py @@ -16,7 +16,8 @@ from traitlets.utils.importstring import import_item from ipython_genutils import py3compat from jinja2 import ( - TemplateNotFound, Environment, ChoiceLoader, FileSystemLoader, BaseLoader + TemplateNotFound, Environment, ChoiceLoader, FileSystemLoader, BaseLoader, + DictLoader ) from nbconvert import filters @@ -159,6 +160,26 @@ def _template_file_changed(self, change): def _template_file_default(self): return self.default_template + self._raw_template = '' + + @property + def raw_template(self): + return self._raw_template + + + @raw_template.setter + def raw_template(self, value): + if value: + _template_name = "" + raw_loader = DictLoader({ + _template_name: value + }) + self.extra_loaders.append(raw_loader) + self.template_file = _template_name + else: + self._raw_template = '' + + default_template = Unicode(u'').tag(affects_template=True) template_path = List(['.']).tag(config=True, affects_environment=True) From cc03ee7e21b0cfe2074a36996e2ff5673e339175 Mon Sep 17 00:00:00 2001 From: M Pacer Date: Wed, 13 Sep 2017 18:46:46 -0700 Subject: [PATCH 007/671] uses raw_template traitlet, creates a register helper and observe --- nbconvert/exporters/templateexporter.py | 33 ++++++++++++------------- 1 file changed, 16 insertions(+), 17 deletions(-) diff --git a/nbconvert/exporters/templateexporter.py b/nbconvert/exporters/templateexporter.py index ddafc73f8..1c1313608 100644 --- a/nbconvert/exporters/templateexporter.py +++ b/nbconvert/exporters/templateexporter.py @@ -141,6 +141,9 @@ def default_config(self): help="Name of the template file to use" ).tag(config=True, affects_template=True) + raw_template = Unicode('', help="raw template string" + ).tag(affects_template=True, affects_environment=True) + @observe('template_file') def _template_file_changed(self, change): new = change['new'] @@ -160,24 +163,20 @@ def _template_file_changed(self, change): def _template_file_default(self): return self.default_template - self._raw_template = '' - - @property - def raw_template(self): - return self._raw_template - - - @raw_template.setter - def raw_template(self, value): - if value: - _template_name = "" - raw_loader = DictLoader({ - _template_name: value - }) - self.extra_loaders.append(raw_loader) - self.template_file = _template_name + def _register_raw_template(self, value): + _template_name = "" + raw_loader = DictLoader({ + _template_name: value + }) + self.extra_loaders.append(raw_loader) + self.template_file = _template_name + + @observe('raw_template') + def _raw_template_changed(self, change): + if change['new']: + self._register_raw_template(change['new']) else: - self._raw_template = '' + self.raw_template = '' default_template = Unicode(u'').tag(affects_template=True) From 4db83b4a6c6837db1b4f9e42c2d7111305b935c9 Mon Sep 17 00:00:00 2001 From: M Pacer Date: Wed, 13 Sep 2017 18:49:43 -0700 Subject: [PATCH 008/671] check if raw_template is set inside _load_template --- nbconvert/exporters/templateexporter.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/nbconvert/exporters/templateexporter.py b/nbconvert/exporters/templateexporter.py index 1c1313608..84500da04 100644 --- a/nbconvert/exporters/templateexporter.py +++ b/nbconvert/exporters/templateexporter.py @@ -274,6 +274,9 @@ def _load_template(self): This is triggered by various trait changes that would change the template. """ + if self.raw_template: + self._register_raw_template(self.raw_template) + if not self.template_file: raise ValueError("No template_file specified!") From 8b027d21cf8268d14280b097349174128de978d6 Mon Sep 17 00:00:00 2001 From: M Pacer Date: Wed, 13 Sep 2017 19:17:29 -0700 Subject: [PATCH 009/671] Add test for 4 ways of setting a raw_template in a class --- nbconvert/exporters/templateexporter.py | 10 +-- .../exporters/tests/test_templateexporter.py | 61 +++++++++++++++---- 2 files changed, 54 insertions(+), 17 deletions(-) diff --git a/nbconvert/exporters/templateexporter.py b/nbconvert/exporters/templateexporter.py index 84500da04..593e2f9b1 100644 --- a/nbconvert/exporters/templateexporter.py +++ b/nbconvert/exporters/templateexporter.py @@ -192,10 +192,10 @@ def _raw_template_changed(self, change): os.path.join("..", "templates", "skeleton"), help="Path where the template skeleton files are located.", ).tag(affects_environment=True) - + #Extension that the template files use. template_extension = Unicode(".tpl").tag(config=True, affects_environment=True) - + exclude_input = Bool(False, help = "This allows you to exclude code cell inputs from all templates if set to True." ).tag(config=True) @@ -227,7 +227,7 @@ def _raw_template_changed(self, change): exclude_unknown = Bool(False, help = "This allows you to exclude unknown cells from all templates if set to True." ).tag(config=True) - + extra_loaders = List( help="Jinja loaders to find templates. Will be tried in order " "before the default FileSystem ones.", @@ -249,7 +249,7 @@ def _raw_mimetypes_default(self): def __init__(self, config=None, **kw): """ Public constructor - + Parameters ---------- config : config @@ -270,7 +270,7 @@ def __init__(self, config=None, **kw): def _load_template(self): """Load the Jinja template object from the template file - + This is triggered by various trait changes that would change the template. """ diff --git a/nbconvert/exporters/tests/test_templateexporter.py b/nbconvert/exporters/tests/test_templateexporter.py index 6496b956e..a62c5e500 100644 --- a/nbconvert/exporters/tests/test_templateexporter.py +++ b/nbconvert/exporters/tests/test_templateexporter.py @@ -7,6 +7,7 @@ import os +from traitlets import default from traitlets.config import Config from jinja2 import DictLoader, TemplateNotFound from nbformat import v4 @@ -20,6 +21,12 @@ import pytest +raw_template = """{%- extends 'rst.tpl' -%} +{%- block in_prompt -%} +blah +{%- endblock in_prompt -%} +""" + class TestExporter(ExportersTestsBase): """Contains test functions for exporter.py""" @@ -119,33 +126,63 @@ def test_relative_template_file(self): exporter = self._make_exporter(config=config) assert os.path.abspath(exporter.template.filename) == template assert os.path.dirname(template) in [os.path.abspath(d) for d in exporter.template_path] - + def test_in_memory_template(self): # Loads in an in memory template using jinja2.DictLoader # creates a class that uses this template with the template_file argument # converts an empty notebook using this mechanism my_loader = DictLoader({'my_template': "{%- extends 'rst.tpl' -%}"}) - + class MyExporter(TemplateExporter): template_file = 'my_template' - + exporter = MyExporter(extra_loaders=[my_loader]) nb = v4.new_notebook() out, resources = exporter.from_notebook_node(nb) + def test_raw_template(self): + + nb = v4.new_notebook() + nb.cells.append(v4.new_code_cell("some_text")) + + class AttrExporter(TemplateExporter): + raw_template = raw_template + + exporter_attr = AttrExporter() + output_attr, _ = exporter_attr.from_notebook_node(nb) + assert "blah" in output_attr + + output_constructor, _ = TemplateExporter( + raw_template=raw_template).from_notebook_node(nb) + assert "blah" in output_constructor + + exporter_assign = TemplateExporter() + exporter_assign.raw_template = raw_template + output_assign, _ = exporter_assign.from_notebook_node(nb) + assert "blah" in output_assign + + class DefaultExporter(TemplateExporter): + @default('raw_template') + def _raw_template_default(self): + return raw_template + + exporter_default = DefaultExporter() + output_default, _ = exporter_default.from_notebook_node(nb) + assert "blah" in output_default + def test_fail_to_find_template_file(self): # Create exporter with invalid template file, check that it doesn't # exist in the environment, try to convert empty notebook. Failure is # expected due to nonexistant template file. - + template = 'does_not_exist.tpl' exporter = TemplateExporter(template_file=template) assert template not in exporter.environment.list_templates(extensions=['tpl']) nb = v4.new_notebook() with pytest.raises(TemplateNotFound): out, resources = exporter.from_notebook_node(nb) - + def test_exclude_code_cell(self): no_io = { "TemplateExporter":{ @@ -161,10 +198,10 @@ def test_exclude_code_cell(self): exporter_no_io = TemplateExporter(config=c_no_io) exporter_no_io.template_file = 'markdown' nb_no_io, resources_no_io = exporter_no_io.from_filename(self._get_notebook()) - + assert not resources_no_io['global_content_filter']['include_input'] assert not resources_no_io['global_content_filter']['include_output'] - + no_code = { "TemplateExporter":{ "exclude_output": False, @@ -183,7 +220,7 @@ def test_exclude_code_cell(self): assert not resources_no_code['global_content_filter']['include_code'] assert nb_no_io == nb_no_code - + def test_exclude_input_prompt(self): no_input_prompt = { "TemplateExporter":{ @@ -198,10 +235,10 @@ def test_exclude_input_prompt(self): c_no_input_prompt = Config(no_input_prompt) exporter_no_input_prompt = MarkdownExporter(config=c_no_input_prompt) nb_no_input_prompt, resources_no_input_prompt = exporter_no_input_prompt.from_filename(self._get_notebook()) - + assert not resources_no_input_prompt['global_content_filter']['include_input_prompt'] assert "# In[" not in nb_no_input_prompt - + def test_exclude_markdown(self): no_md= { @@ -219,10 +256,10 @@ def test_exclude_markdown(self): exporter_no_md = TemplateExporter(config=c_no_md) exporter_no_md.template_file = 'python' nb_no_md, resources_no_md = exporter_no_md.from_filename(self._get_notebook()) - + assert not resources_no_md['global_content_filter']['include_markdown'] assert "First import NumPy and Matplotlib" not in nb_no_md - + def test_exclude_output_prompt(self): no_output_prompt = { "TemplateExporter":{ From dbd0488f29f5a075fd97496d28b0d991043504f4 Mon Sep 17 00:00:00 2001 From: M Pacer Date: Wed, 13 Sep 2017 19:54:00 -0700 Subject: [PATCH 010/671] Remove affects_template tag, store default_template, restore if no raw_template --- nbconvert/exporters/templateexporter.py | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/nbconvert/exporters/templateexporter.py b/nbconvert/exporters/templateexporter.py index 593e2f9b1..ab9413cbf 100644 --- a/nbconvert/exporters/templateexporter.py +++ b/nbconvert/exporters/templateexporter.py @@ -142,7 +142,7 @@ def default_config(self): ).tag(config=True, affects_template=True) raw_template = Unicode('', help="raw template string" - ).tag(affects_template=True, affects_environment=True) + ).tag(affects_environment=True) @observe('template_file') def _template_file_changed(self, change): @@ -164,19 +164,21 @@ def _template_file_default(self): return self.default_template def _register_raw_template(self, value): - _template_name = "" - raw_loader = DictLoader({ - _template_name: value - }) - self.extra_loaders.append(raw_loader) - self.template_file = _template_name + if value: + _template_name = "" + raw_loader = DictLoader({ + _template_name: value + }) + self.extra_loaders.append(raw_loader) + if self.template_file != self.default_template: + self._default_template = self.template_file + self.template_file = _template_name + else: + self.template_file = self.default_template or self._default_template @observe('raw_template') def _raw_template_changed(self, change): - if change['new']: - self._register_raw_template(change['new']) - else: - self.raw_template = '' + self._register_raw_template(change['new']) default_template = Unicode(u'').tag(affects_template=True) From 810d07764019f0e6697a33749ef4e007d72865f6 Mon Sep 17 00:00:00 2001 From: M Pacer Date: Wed, 13 Sep 2017 19:55:12 -0700 Subject: [PATCH 011/671] Add test to verify that an empty raw_template returns back to dynamic default for template_file --- nbconvert/exporters/tests/test_templateexporter.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/nbconvert/exporters/tests/test_templateexporter.py b/nbconvert/exporters/tests/test_templateexporter.py index a62c5e500..438472b20 100644 --- a/nbconvert/exporters/tests/test_templateexporter.py +++ b/nbconvert/exporters/tests/test_templateexporter.py @@ -153,6 +153,18 @@ class AttrExporter(TemplateExporter): output_attr, _ = exporter_attr.from_notebook_node(nb) assert "blah" in output_attr + class AttrRemovedExporter(TemplateExporter): + raw_template = raw_template + + @default('template_file') + def _raw_template_default(self): + return "rst.tpl" + + exporter_attr_removed = AttrRemovedExporter() + exporter_attr_removed.raw_template = '' + output_attr_removed, _ = exporter_attr_removed.from_notebook_node(nb) + assert "blah" not in output_attr_removed + output_constructor, _ = TemplateExporter( raw_template=raw_template).from_notebook_node(nb) assert "blah" in output_constructor From c3cfde87b62f617f4d72daeb5b6c8964d68a9a3e Mon Sep 17 00:00:00 2001 From: M Pacer Date: Thu, 14 Sep 2017 02:04:30 -0700 Subject: [PATCH 012/671] use attribute for _raw_template_key as a precursor to not appending DictLoader every time --- nbconvert/exporters/templateexporter.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/nbconvert/exporters/templateexporter.py b/nbconvert/exporters/templateexporter.py index ab9413cbf..23717182b 100644 --- a/nbconvert/exporters/templateexporter.py +++ b/nbconvert/exporters/templateexporter.py @@ -144,6 +144,8 @@ def default_config(self): raw_template = Unicode('', help="raw template string" ).tag(affects_environment=True) + _raw_template_key = "" + @observe('template_file') def _template_file_changed(self, change): new = change['new'] @@ -165,14 +167,13 @@ def _template_file_default(self): def _register_raw_template(self, value): if value: - _template_name = "" raw_loader = DictLoader({ - _template_name: value + self._raw_template_key: value }) self.extra_loaders.append(raw_loader) if self.template_file != self.default_template: self._default_template = self.template_file - self.template_file = _template_name + self.template_file = self._raw_template_key else: self.template_file = self.default_template or self._default_template @@ -276,6 +277,7 @@ def _load_template(self): This is triggered by various trait changes that would change the template. """ + # this gives precedence to a raw_template if present if self.raw_template: self._register_raw_template(self.raw_template) From 449ae22d6044ca7b4b4a7018685a7fe62a70a830 Mon Sep 17 00:00:00 2001 From: M Pacer Date: Thu, 14 Sep 2017 02:05:25 -0700 Subject: [PATCH 013/671] Change dynamic test to only work if set as a traitlet per @takluyver's suggestion --- .../exporters/tests/test_templateexporter.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/nbconvert/exporters/tests/test_templateexporter.py b/nbconvert/exporters/tests/test_templateexporter.py index 438472b20..705b96aea 100644 --- a/nbconvert/exporters/tests/test_templateexporter.py +++ b/nbconvert/exporters/tests/test_templateexporter.py @@ -153,17 +153,22 @@ class AttrExporter(TemplateExporter): output_attr, _ = exporter_attr.from_notebook_node(nb) assert "blah" in output_attr - class AttrRemovedExporter(TemplateExporter): - raw_template = raw_template + class AttrDynamicExporter(TemplateExporter): + @default('raw_template') + def _raw_template_default(self): + return raw_template @default('template_file') - def _raw_template_default(self): + def _template_file_default(self): return "rst.tpl" - exporter_attr_removed = AttrRemovedExporter() - exporter_attr_removed.raw_template = '' - output_attr_removed, _ = exporter_attr_removed.from_notebook_node(nb) - assert "blah" not in output_attr_removed + exporter_attr_dynamic = AttrDynamicExporter() + output_attr_dynamic, _ = exporter_attr_dynamic.from_notebook_node(nb) + assert exporter_attr_dynamic.template_file != "rst.tpl" + exporter_attr_dynamic.raw_template = '' + assert exporter_attr_dynamic.template_file == "rst.tpl" + output_attr_dynamic, _ = exporter_attr_dynamic.from_notebook_node(nb) + assert "blah" not in output_attr_dynamic output_constructor, _ = TemplateExporter( raw_template=raw_template).from_notebook_node(nb) From ea52c9154fb6a1a520b91b4afa15d622bf789f0b Mon Sep 17 00:00:00 2001 From: M Pacer Date: Thu, 14 Sep 2017 02:53:32 -0700 Subject: [PATCH 014/671] use FunctionLoader not DictLoader so we can mutate values inside the class --- nbconvert/exporters/templateexporter.py | 31 ++++++++++++++++++++----- 1 file changed, 25 insertions(+), 6 deletions(-) diff --git a/nbconvert/exporters/templateexporter.py b/nbconvert/exporters/templateexporter.py index 23717182b..cf095fa5d 100644 --- a/nbconvert/exporters/templateexporter.py +++ b/nbconvert/exporters/templateexporter.py @@ -17,7 +17,7 @@ from ipython_genutils import py3compat from jinja2 import ( TemplateNotFound, Environment, ChoiceLoader, FileSystemLoader, BaseLoader, - DictLoader + FunctionLoader ) from nbconvert import filters @@ -58,6 +58,19 @@ 'json_dumps': json.dumps, } +class ExplicitFunctionLoader(FunctionLoader): + def __init__(self, load_func, template_list=None): + super(ExplicitFunctionLoader, self).__init__(load_func) + if isinstance(template_list, list): + self.template_list = template_list + else: + self.template_list = None + + def list_templates(self): + if self.template_list: + return sorted(self.template_list) + else: + super(ExplicitFunctionLoader, self).list_templates() class ExtensionTolerantLoader(BaseLoader): """A template loader which optionally adds a given extension when searching. @@ -145,6 +158,7 @@ def default_config(self): ).tag(affects_environment=True) _raw_template_key = "" + _raw_template_content = "" @observe('template_file') def _template_file_changed(self, change): @@ -165,12 +179,15 @@ def _template_file_changed(self, change): def _template_file_default(self): return self.default_template + def _load_raw_template(self, name): + if name == self._raw_template_key: + return self._raw_template_content, None, False + else: + return None + def _register_raw_template(self, value): if value: - raw_loader = DictLoader({ - self._raw_template_key: value - }) - self.extra_loaders.append(raw_loader) + self._raw_template_content = value if self.template_file != self.default_template: self._default_template = self.template_file self.template_file = self._raw_template_key @@ -404,7 +421,9 @@ def _create_environment(self): os.path.join(here, self.template_skeleton_path)] loaders = self.extra_loaders + [ - ExtensionTolerantLoader(FileSystemLoader(paths), self.template_extension) + ExtensionTolerantLoader(FileSystemLoader(paths), self.template_extension), + ExplicitFunctionLoader(self._load_raw_template, + template_list=[self._raw_template_key]) ] environment = Environment( loader=ChoiceLoader(loaders), From b335d9c3d3aa81b704949653dcef43c427ed90d3 Mon Sep 17 00:00:00 2001 From: M Pacer Date: Thu, 14 Sep 2017 15:02:35 -0700 Subject: [PATCH 015/671] split up tests and explicitly test for traitlet related order effects --- .../exporters/tests/test_templateexporter.py | 53 ++++++++++++++----- 1 file changed, 41 insertions(+), 12 deletions(-) diff --git a/nbconvert/exporters/tests/test_templateexporter.py b/nbconvert/exporters/tests/test_templateexporter.py index 705b96aea..54843026f 100644 --- a/nbconvert/exporters/tests/test_templateexporter.py +++ b/nbconvert/exporters/tests/test_templateexporter.py @@ -141,7 +141,7 @@ class MyExporter(TemplateExporter): out, resources = exporter.from_notebook_node(nb) - def test_raw_template(self): + def test_raw_template_attr_overwrite(self): nb = v4.new_notebook() nb.cells.append(v4.new_code_cell("some_text")) @@ -153,7 +153,35 @@ class AttrExporter(TemplateExporter): output_attr, _ = exporter_attr.from_notebook_node(nb) assert "blah" in output_attr + def test_raw_template_dynamic_attr(self): + nb = v4.new_notebook() + nb.cells.append(v4.new_code_cell("some_text")) + class AttrDynamicExporter(TemplateExporter): + @default('template_file') + def _template_file_default(self): + return "rst.tpl" + + @default('raw_template') + def _raw_template_default(self): + return raw_template + + + exporter_attr_dynamic = AttrDynamicExporter() + # import pdb; pdb.set_trace() + output_attr_dynamic, _ = exporter_attr_dynamic.from_notebook_node(nb) + # assert exporter_attr_dynamic.template_file != "rst.tpl" + assert "blah" in output_attr_dynamic + exporter_attr_dynamic.raw_template = '' + assert exporter_attr_dynamic.template_file == "rst.tpl" + output_attr_dynamic, _ = exporter_attr_dynamic.from_notebook_node(nb) + assert "blah" not in output_attr_dynamic + + def test_raw_template_dynamic_attr_2(self): + nb = v4.new_notebook() + nb.cells.append(v4.new_code_cell("some_text")) + + class AttrDynamicExporter_2(TemplateExporter): @default('raw_template') def _raw_template_default(self): return raw_template @@ -162,32 +190,33 @@ def _raw_template_default(self): def _template_file_default(self): return "rst.tpl" - exporter_attr_dynamic = AttrDynamicExporter() + + + exporter_attr_dynamic = AttrDynamicExporter_2() + # import pdb; pdb.set_trace() output_attr_dynamic, _ = exporter_attr_dynamic.from_notebook_node(nb) - assert exporter_attr_dynamic.template_file != "rst.tpl" + # assert exporter_attr_dynamic.template_file != "rst.tpl" + assert "blah" in output_attr_dynamic exporter_attr_dynamic.raw_template = '' assert exporter_attr_dynamic.template_file == "rst.tpl" output_attr_dynamic, _ = exporter_attr_dynamic.from_notebook_node(nb) assert "blah" not in output_attr_dynamic + def test_raw_template_constructor(self): + nb = v4.new_notebook() + nb.cells.append(v4.new_code_cell("some_text")) output_constructor, _ = TemplateExporter( raw_template=raw_template).from_notebook_node(nb) assert "blah" in output_constructor + def test_raw_template_assignment(self): + nb = v4.new_notebook() + nb.cells.append(v4.new_code_cell("some_text")) exporter_assign = TemplateExporter() exporter_assign.raw_template = raw_template output_assign, _ = exporter_assign.from_notebook_node(nb) assert "blah" in output_assign - class DefaultExporter(TemplateExporter): - @default('raw_template') - def _raw_template_default(self): - return raw_template - - exporter_default = DefaultExporter() - output_default, _ = exporter_default.from_notebook_node(nb) - assert "blah" in output_default - def test_fail_to_find_template_file(self): # Create exporter with invalid template file, check that it doesn't # exist in the environment, try to convert empty notebook. Failure is From 728c15bb16b5dbc8ae5d5aea57d3c48f71772efd Mon Sep 17 00:00:00 2001 From: M Pacer Date: Thu, 14 Sep 2017 15:07:02 -0700 Subject: [PATCH 016/671] clean up tests, remove old in memory test that is now super redundant --- .../exporters/tests/test_templateexporter.py | 28 ++++--------------- 1 file changed, 5 insertions(+), 23 deletions(-) diff --git a/nbconvert/exporters/tests/test_templateexporter.py b/nbconvert/exporters/tests/test_templateexporter.py index 54843026f..f58bab6dc 100644 --- a/nbconvert/exporters/tests/test_templateexporter.py +++ b/nbconvert/exporters/tests/test_templateexporter.py @@ -127,19 +127,6 @@ def test_relative_template_file(self): assert os.path.abspath(exporter.template.filename) == template assert os.path.dirname(template) in [os.path.abspath(d) for d in exporter.template_path] - def test_in_memory_template(self): - # Loads in an in memory template using jinja2.DictLoader - # creates a class that uses this template with the template_file argument - # converts an empty notebook using this mechanism - my_loader = DictLoader({'my_template': "{%- extends 'rst.tpl' -%}"}) - - class MyExporter(TemplateExporter): - template_file = 'my_template' - - exporter = MyExporter(extra_loaders=[my_loader]) - nb = v4.new_notebook() - out, resources = exporter.from_notebook_node(nb) - def test_raw_template_attr_overwrite(self): @@ -166,22 +153,19 @@ def _template_file_default(self): def _raw_template_default(self): return raw_template - exporter_attr_dynamic = AttrDynamicExporter() - # import pdb; pdb.set_trace() output_attr_dynamic, _ = exporter_attr_dynamic.from_notebook_node(nb) - # assert exporter_attr_dynamic.template_file != "rst.tpl" assert "blah" in output_attr_dynamic exporter_attr_dynamic.raw_template = '' assert exporter_attr_dynamic.template_file == "rst.tpl" output_attr_dynamic, _ = exporter_attr_dynamic.from_notebook_node(nb) assert "blah" not in output_attr_dynamic - def test_raw_template_dynamic_attr_2(self): + def test_raw_template_dynamic_attr_reversed(self): nb = v4.new_notebook() nb.cells.append(v4.new_code_cell("some_text")) - class AttrDynamicExporter_2(TemplateExporter): + class AttrDynamicExporter(TemplateExporter): @default('raw_template') def _raw_template_default(self): return raw_template @@ -190,17 +174,15 @@ def _raw_template_default(self): def _template_file_default(self): return "rst.tpl" - - - exporter_attr_dynamic = AttrDynamicExporter_2() - # import pdb; pdb.set_trace() + exporter_attr_dynamic = AttrDynamicExporter() output_attr_dynamic, _ = exporter_attr_dynamic.from_notebook_node(nb) - # assert exporter_attr_dynamic.template_file != "rst.tpl" assert "blah" in output_attr_dynamic exporter_attr_dynamic.raw_template = '' assert exporter_attr_dynamic.template_file == "rst.tpl" output_attr_dynamic, _ = exporter_attr_dynamic.from_notebook_node(nb) assert "blah" not in output_attr_dynamic + + def test_raw_template_constructor(self): nb = v4.new_notebook() nb.cells.append(v4.new_code_cell("some_text")) From 6157733983354c467075ca5f68b6754c8e7aa73d Mon Sep 17 00:00:00 2001 From: M Pacer Date: Thu, 14 Sep 2017 15:24:08 -0700 Subject: [PATCH 017/671] add docstrings to all new tests --- .../exporters/tests/test_templateexporter.py | 25 +++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/nbconvert/exporters/tests/test_templateexporter.py b/nbconvert/exporters/tests/test_templateexporter.py index f58bab6dc..fbf4b8ccc 100644 --- a/nbconvert/exporters/tests/test_templateexporter.py +++ b/nbconvert/exporters/tests/test_templateexporter.py @@ -128,8 +128,11 @@ def test_relative_template_file(self): assert os.path.dirname(template) in [os.path.abspath(d) for d in exporter.template_path] - def test_raw_template_attr_overwrite(self): - + def test_raw_template_attr(self): + """ + Verify that you can assign a in memory template string by overwriting + `raw_template` as simple(non-traitlet) attribute + """ nb = v4.new_notebook() nb.cells.append(v4.new_code_cell("some_text")) @@ -141,6 +144,12 @@ class AttrExporter(TemplateExporter): assert "blah" in output_attr def test_raw_template_dynamic_attr(self): + """ + Test that template_file and raw_template traitlets play nicely together. + - source assigns template_file default first, then raw_template + - checks that the raw_template overrules template_file if set + - checks that once raw_template is set to '', template_file returns + """ nb = v4.new_notebook() nb.cells.append(v4.new_code_cell("some_text")) @@ -162,6 +171,12 @@ def _raw_template_default(self): assert "blah" not in output_attr_dynamic def test_raw_template_dynamic_attr_reversed(self): + """ + Test that template_file and raw_template traitlets play nicely together. + - source assigns raw_template default first, then template_file + - checks that the raw_template overrules template_file if set + - checks that once raw_template is set to '', template_file returns + """ nb = v4.new_notebook() nb.cells.append(v4.new_code_cell("some_text")) @@ -184,6 +199,9 @@ def _template_file_default(self): def test_raw_template_constructor(self): + """ + Test `raw_template` as a keyword argument in the exporter constructor. + """ nb = v4.new_notebook() nb.cells.append(v4.new_code_cell("some_text")) @@ -192,6 +210,9 @@ def test_raw_template_constructor(self): assert "blah" in output_constructor def test_raw_template_assignment(self): + """ + Test `raw_template` assigned after the fact on non-custom Exporter. + """ nb = v4.new_notebook() nb.cells.append(v4.new_code_cell("some_text")) exporter_assign = TemplateExporter() From b79f0d81c04d51db840d36a155fd9443ab8d87f7 Mon Sep 17 00:00:00 2001 From: M Pacer Date: Thu, 14 Sep 2017 15:29:43 -0700 Subject: [PATCH 018/671] simplify logic using hold_trait_notifications() ftw (avoids race conditions) --- nbconvert/exporters/templateexporter.py | 21 ++++++++------------- 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/nbconvert/exporters/templateexporter.py b/nbconvert/exporters/templateexporter.py index cf095fa5d..3d88ceeda 100644 --- a/nbconvert/exporters/templateexporter.py +++ b/nbconvert/exporters/templateexporter.py @@ -158,7 +158,7 @@ def default_config(self): ).tag(affects_environment=True) _raw_template_key = "" - _raw_template_content = "" + _last_template_file = Unicode("", help="holder for last template_file") @observe('template_file') def _template_file_changed(self, change): @@ -181,22 +181,15 @@ def _template_file_default(self): def _load_raw_template(self, name): if name == self._raw_template_key: - return self._raw_template_content, None, False + return self.raw_template, None, False else: return None - def _register_raw_template(self, value): - if value: - self._raw_template_content = value - if self.template_file != self.default_template: - self._default_template = self.template_file - self.template_file = self._raw_template_key - else: - self.template_file = self.default_template or self._default_template @observe('raw_template') def _raw_template_changed(self, change): - self._register_raw_template(change['new']) + if not change['new']: + self.template_file = self.default_template or self._last_template_file default_template = Unicode(u'').tag(affects_template=True) @@ -295,8 +288,10 @@ def _load_template(self): """ # this gives precedence to a raw_template if present - if self.raw_template: - self._register_raw_template(self.raw_template) + with self.hold_trait_notifications(): + if self.raw_template: + self._last_template_file = self.template_file + self.template_file = self._raw_template_key if not self.template_file: raise ValueError("No template_file specified!") From d1bbd81bd8451240861d0751dd00c2d4c8766534 Mon Sep 17 00:00:00 2001 From: M Pacer Date: Thu, 14 Sep 2017 15:35:20 -0700 Subject: [PATCH 019/671] make raw_template_key configurable as backdoor in the case of filename conflicts --- nbconvert/exporters/templateexporter.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/nbconvert/exporters/templateexporter.py b/nbconvert/exporters/templateexporter.py index 3d88ceeda..5f29fa6a5 100644 --- a/nbconvert/exporters/templateexporter.py +++ b/nbconvert/exporters/templateexporter.py @@ -154,11 +154,16 @@ def default_config(self): help="Name of the template file to use" ).tag(config=True, affects_template=True) - raw_template = Unicode('', help="raw template string" + raw_template = Unicode('', + help="raw template string" ).tag(affects_environment=True) - _raw_template_key = "" _last_template_file = Unicode("", help="holder for last template_file") + raw_template_key = Unicode("", + help=("pseudo filename for in-memory template assignment. " + "It is suggested that you do not change this unless you run into " + "conflicts with the default value.") + ).tag(config=True) @observe('template_file') def _template_file_changed(self, change): @@ -180,7 +185,7 @@ def _template_file_default(self): return self.default_template def _load_raw_template(self, name): - if name == self._raw_template_key: + if name == self.raw_template_key: return self.raw_template, None, False else: return None @@ -291,7 +296,7 @@ def _load_template(self): with self.hold_trait_notifications(): if self.raw_template: self._last_template_file = self.template_file - self.template_file = self._raw_template_key + self.template_file = self.raw_template_key if not self.template_file: raise ValueError("No template_file specified!") @@ -418,7 +423,7 @@ def _create_environment(self): loaders = self.extra_loaders + [ ExtensionTolerantLoader(FileSystemLoader(paths), self.template_extension), ExplicitFunctionLoader(self._load_raw_template, - template_list=[self._raw_template_key]) + template_list=[self.raw_template_key]) ] environment = Environment( loader=ChoiceLoader(loaders), From eee0766d8a709db161b885ae50260b89d67ceb7b Mon Sep 17 00:00:00 2001 From: Thomas Kluyver Date: Fri, 15 Sep 2017 12:03:22 +0100 Subject: [PATCH 020/671] Utility function for prepending to an env search path --- nbconvert/exporters/pdf.py | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/nbconvert/exporters/pdf.py b/nbconvert/exporters/pdf.py index 6de07d0da..eea606a40 100644 --- a/nbconvert/exporters/pdf.py +++ b/nbconvert/exporters/pdf.py @@ -27,6 +27,18 @@ def __str__(self): u = self.__unicode__() return cast_bytes_py2(u) +def prepend_to_env_search_path(varname, value, envdict): + """Add value to the environment variable varname in envdict + + e.g. prepend_to_env_search_path('BIBINPUTS', '/home/sally/foo', os.environ) + """ + if not value: + return # Nothing to add + + if varname not in envdict: + envdict[varname] = cast_bytes_py2(value) + else: + envdict[varname] = cast_bytes_py2(value) + os.pathsep + envdict[varname] class PDFExporter(LatexExporter): """Writer designed to write to PDF files. @@ -105,10 +117,8 @@ def run_command(self, command_list, filename, count, log_function): if shell: command = subprocess.list2cmdline(command) env = os.environ.copy() - env['TEXINPUTS'] = os.pathsep.join([ - cast_bytes_py2(self.texinputs), - env.get('TEXINPUTS', ''), - ]) + prepend_to_env_search_path('TEXINPUTS', self.texinputs, env) + with open(os.devnull, 'rb') as null: stdout = subprocess.PIPE if not self.verbose else None for index in range(count): From e239b78073963b4462c07f396515cffdf008c8a3 Mon Sep 17 00:00:00 2001 From: Thomas Kluyver Date: Fri, 15 Sep 2017 12:04:55 +0100 Subject: [PATCH 021/671] Also set BIBINPUTS and BSTINPUTS environment variables --- nbconvert/exporters/pdf.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/nbconvert/exporters/pdf.py b/nbconvert/exporters/pdf.py index eea606a40..85e09f877 100644 --- a/nbconvert/exporters/pdf.py +++ b/nbconvert/exporters/pdf.py @@ -118,6 +118,8 @@ def run_command(self, command_list, filename, count, log_function): command = subprocess.list2cmdline(command) env = os.environ.copy() prepend_to_env_search_path('TEXINPUTS', self.texinputs, env) + prepend_to_env_search_path('BIBINPUTS', self.texinputs, env) + prepend_to_env_search_path('BSTINPUTS', self.texinputs, env) with open(os.devnull, 'rb') as null: stdout = subprocess.PIPE if not self.verbose else None From 0d9d70fd4d7eb46bcede4fec8365eea9f25b4afd Mon Sep 17 00:00:00 2001 From: Thomas Kluyver Date: Fri, 15 Sep 2017 12:29:14 +0100 Subject: [PATCH 022/671] Ensure CWD is also on TeX search path --- nbconvert/exporters/pdf.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/nbconvert/exporters/pdf.py b/nbconvert/exporters/pdf.py index 85e09f877..7ee7a50e5 100644 --- a/nbconvert/exporters/pdf.py +++ b/nbconvert/exporters/pdf.py @@ -35,10 +35,7 @@ def prepend_to_env_search_path(varname, value, envdict): if not value: return # Nothing to add - if varname not in envdict: - envdict[varname] = cast_bytes_py2(value) - else: - envdict[varname] = cast_bytes_py2(value) + os.pathsep + envdict[varname] + envdict[varname] = cast_bytes_py2(value) + os.pathsep + envdict.get(varname, '') class PDFExporter(LatexExporter): """Writer designed to write to PDF files. From 738673af8d7dca78aba38644e431358ffa858274 Mon Sep 17 00:00:00 2001 From: M Pacer Date: Sat, 16 Sep 2017 15:15:02 -0700 Subject: [PATCH 023/671] add test for reassignment --- .../exporters/tests/test_templateexporter.py | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/nbconvert/exporters/tests/test_templateexporter.py b/nbconvert/exporters/tests/test_templateexporter.py index fbf4b8ccc..f241f2e71 100644 --- a/nbconvert/exporters/tests/test_templateexporter.py +++ b/nbconvert/exporters/tests/test_templateexporter.py @@ -15,6 +15,7 @@ from .base import ExportersTestsBase from .cheese import CheesePreprocessor from ..templateexporter import TemplateExporter +from ..rst import RSTExporter from ..html import HTMLExporter from ..markdown import MarkdownExporter from testpath import tempdir @@ -143,6 +144,29 @@ class AttrExporter(TemplateExporter): output_attr, _ = exporter_attr.from_notebook_node(nb) assert "blah" in output_attr + def test_raw_template_init(self): + """ + Test that template_file and raw_template traitlets play nicely together. + - source assigns template_file default first, then raw_template + - checks that the raw_template overrules template_file if set + - checks that once raw_template is set to '', template_file returns + """ + nb = v4.new_notebook() + nb.cells.append(v4.new_code_cell("some_text")) + + class AttrExporter(RSTExporter): + + def __init__(self, *args, **kwargs): + self.raw_template = raw_template + + exporter_init = AttrExporter() + output_init, _ = exporter_init.from_notebook_node(nb) + assert "blah" in output_init + exporter_init.raw_template = '' + assert exporter_init.template_file == "rst.tpl" + output_init, _ = exporter_init.from_notebook_node(nb) + assert "blah" not in output_init + def test_raw_template_dynamic_attr(self): """ Test that template_file and raw_template traitlets play nicely together. @@ -220,6 +244,20 @@ def test_raw_template_assignment(self): output_assign, _ = exporter_assign.from_notebook_node(nb) assert "blah" in output_assign + def test_raw_template_reassignment(self): + """ + Test `raw_template` assigned after the fact on non-custom Exporter. + """ + nb = v4.new_notebook() + nb.cells.append(v4.new_code_cell("some_text")) + exporter_reassign = TemplateExporter() + exporter_reassign.raw_template = raw_template + output_reassign, _ = exporter_reassign.from_notebook_node(nb) + assert "blah" in output_reassign + exporter_reassign.raw_template = raw_template.replace("blah", "baz") + output_reassign, _ = exporter_reassign.from_notebook_node(nb) + assert "baz" in output_reassign + def test_fail_to_find_template_file(self): # Create exporter with invalid template file, check that it doesn't # exist in the environment, try to convert empty notebook. Failure is From 2975c1b81425ef5d2a710323bfc3b71e42aeab09 Mon Sep 17 00:00:00 2001 From: M Pacer Date: Sat, 16 Sep 2017 15:26:51 -0700 Subject: [PATCH 024/671] Make _last_template_file a simple attribute, invalidate cache when setting raw_template --- nbconvert/exporters/templateexporter.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/nbconvert/exporters/templateexporter.py b/nbconvert/exporters/templateexporter.py index 5f29fa6a5..2283046d8 100644 --- a/nbconvert/exporters/templateexporter.py +++ b/nbconvert/exporters/templateexporter.py @@ -154,11 +154,9 @@ def default_config(self): help="Name of the template file to use" ).tag(config=True, affects_template=True) - raw_template = Unicode('', - help="raw template string" - ).tag(affects_environment=True) + raw_template = Unicode('', help="raw template string") - _last_template_file = Unicode("", help="holder for last template_file") + _last_template_file = "" raw_template_key = Unicode("", help=("pseudo filename for in-memory template assignment. " "It is suggested that you do not change this unless you run into " @@ -190,12 +188,11 @@ def _load_raw_template(self, name): else: return None - @observe('raw_template') def _raw_template_changed(self, change): if not change['new']: self.template_file = self.default_template or self._last_template_file - + self._invalidate_template_cache() default_template = Unicode(u'').tag(affects_template=True) From 34074a5971037a215c1b9a08052b9ef49fdc54c9 Mon Sep 17 00:00:00 2001 From: M Pacer Date: Sat, 16 Sep 2017 15:27:44 -0700 Subject: [PATCH 025/671] use dictloader and add back affects_environment tag to raw_template --- nbconvert/exporters/templateexporter.py | 21 +++------------------ 1 file changed, 3 insertions(+), 18 deletions(-) diff --git a/nbconvert/exporters/templateexporter.py b/nbconvert/exporters/templateexporter.py index 2283046d8..acdf8d964 100644 --- a/nbconvert/exporters/templateexporter.py +++ b/nbconvert/exporters/templateexporter.py @@ -17,7 +17,7 @@ from ipython_genutils import py3compat from jinja2 import ( TemplateNotFound, Environment, ChoiceLoader, FileSystemLoader, BaseLoader, - FunctionLoader + DictLoader ) from nbconvert import filters @@ -58,20 +58,6 @@ 'json_dumps': json.dumps, } -class ExplicitFunctionLoader(FunctionLoader): - def __init__(self, load_func, template_list=None): - super(ExplicitFunctionLoader, self).__init__(load_func) - if isinstance(template_list, list): - self.template_list = template_list - else: - self.template_list = None - - def list_templates(self): - if self.template_list: - return sorted(self.template_list) - else: - super(ExplicitFunctionLoader, self).list_templates() - class ExtensionTolerantLoader(BaseLoader): """A template loader which optionally adds a given extension when searching. @@ -154,7 +140,7 @@ def default_config(self): help="Name of the template file to use" ).tag(config=True, affects_template=True) - raw_template = Unicode('', help="raw template string") + raw_template = Unicode('', help="raw template string").tag(affects_environment=True) _last_template_file = "" raw_template_key = Unicode("", @@ -419,8 +405,7 @@ def _create_environment(self): loaders = self.extra_loaders + [ ExtensionTolerantLoader(FileSystemLoader(paths), self.template_extension), - ExplicitFunctionLoader(self._load_raw_template, - template_list=[self.raw_template_key]) + DictLoader({self.raw_template_key: self.raw_template}) ] environment = Environment( loader=ChoiceLoader(loaders), From c9008294509930ddf52bdc97245f9a7e4cb87ca8 Mon Sep 17 00:00:00 2001 From: M Pacer Date: Sat, 16 Sep 2017 15:45:00 -0700 Subject: [PATCH 026/671] don't make raw_template_key configurable or public --- nbconvert/exporters/templateexporter.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/nbconvert/exporters/templateexporter.py b/nbconvert/exporters/templateexporter.py index acdf8d964..bd2488bcf 100644 --- a/nbconvert/exporters/templateexporter.py +++ b/nbconvert/exporters/templateexporter.py @@ -143,11 +143,7 @@ def default_config(self): raw_template = Unicode('', help="raw template string").tag(affects_environment=True) _last_template_file = "" - raw_template_key = Unicode("", - help=("pseudo filename for in-memory template assignment. " - "It is suggested that you do not change this unless you run into " - "conflicts with the default value.") - ).tag(config=True) + _raw_template_key = "" @observe('template_file') def _template_file_changed(self, change): @@ -169,7 +165,7 @@ def _template_file_default(self): return self.default_template def _load_raw_template(self, name): - if name == self.raw_template_key: + if name == self._raw_template_key: return self.raw_template, None, False else: return None @@ -279,7 +275,7 @@ def _load_template(self): with self.hold_trait_notifications(): if self.raw_template: self._last_template_file = self.template_file - self.template_file = self.raw_template_key + self.template_file = self._raw_template_key if not self.template_file: raise ValueError("No template_file specified!") @@ -405,7 +401,7 @@ def _create_environment(self): loaders = self.extra_loaders + [ ExtensionTolerantLoader(FileSystemLoader(paths), self.template_extension), - DictLoader({self.raw_template_key: self.raw_template}) + DictLoader({self._raw_template_key: self.raw_template}) ] environment = Environment( loader=ChoiceLoader(loaders), From d23c7e0ec79ff8b78800684d37541ee8ee0f1a83 Mon Sep 17 00:00:00 2001 From: M Pacer Date: Sat, 16 Sep 2017 15:51:42 -0700 Subject: [PATCH 027/671] test deassignment on a non-custom exporter --- .../exporters/tests/test_templateexporter.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/nbconvert/exporters/tests/test_templateexporter.py b/nbconvert/exporters/tests/test_templateexporter.py index f241f2e71..b819be8e5 100644 --- a/nbconvert/exporters/tests/test_templateexporter.py +++ b/nbconvert/exporters/tests/test_templateexporter.py @@ -246,7 +246,7 @@ def test_raw_template_assignment(self): def test_raw_template_reassignment(self): """ - Test `raw_template` assigned after the fact on non-custom Exporter. + Test `raw_template` reassigned after the fact on non-custom Exporter. """ nb = v4.new_notebook() nb.cells.append(v4.new_code_cell("some_text")) @@ -258,6 +258,22 @@ def test_raw_template_reassignment(self): output_reassign, _ = exporter_reassign.from_notebook_node(nb) assert "baz" in output_reassign + def test_raw_template_deassignment(self): + """ + Test `raw_template` does not overwrite template_file if deassigned after + being assigned to a non-custom Exporter. + """ + nb = v4.new_notebook() + nb.cells.append(v4.new_code_cell("some_text")) + exporter_deassign = RSTExporter() + exporter_deassign.raw_template = raw_template + output_deassign, _ = exporter_deassign.from_notebook_node(nb) + assert "blah" in output_deassign + exporter_deassign.raw_template = '' + assert exporter_deassign.template_file == 'rst.tpl' + output_deassign, _ = exporter_deassign.from_notebook_node(nb) + assert "blah" not in output_deassign + def test_fail_to_find_template_file(self): # Create exporter with invalid template file, check that it doesn't # exist in the environment, try to convert empty notebook. Failure is From 7a923604e61d057bf3de407c7e0b270fcb9defb1 Mon Sep 17 00:00:00 2001 From: M Pacer Date: Sun, 17 Sep 2017 12:02:53 -0700 Subject: [PATCH 028/671] Handle reassigning and then deassigning new raw templates and getting back the last template_file --- nbconvert/exporters/templateexporter.py | 3 ++- .../exporters/tests/test_templateexporter.py | 19 +++++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/nbconvert/exporters/templateexporter.py b/nbconvert/exporters/templateexporter.py index bd2488bcf..a3e2056d9 100644 --- a/nbconvert/exporters/templateexporter.py +++ b/nbconvert/exporters/templateexporter.py @@ -273,8 +273,9 @@ def _load_template(self): # this gives precedence to a raw_template if present with self.hold_trait_notifications(): - if self.raw_template: + if self.template_file != self._raw_template_key: self._last_template_file = self.template_file + if self.raw_template: self.template_file = self._raw_template_key if not self.template_file: diff --git a/nbconvert/exporters/tests/test_templateexporter.py b/nbconvert/exporters/tests/test_templateexporter.py index b819be8e5..0a57b6ebe 100644 --- a/nbconvert/exporters/tests/test_templateexporter.py +++ b/nbconvert/exporters/tests/test_templateexporter.py @@ -274,6 +274,25 @@ def test_raw_template_deassignment(self): output_deassign, _ = exporter_deassign.from_notebook_node(nb) assert "blah" not in output_deassign + def test_raw_template_dereassignment(self): + """ + Test `raw_template` does not overwrite template_file if deassigned after + being assigned to a non-custom Exporter. + """ + nb = v4.new_notebook() + nb.cells.append(v4.new_code_cell("some_text")) + exporter_dereassign = RSTExporter() + exporter_dereassign.raw_template = raw_template + output_dereassign, _ = exporter_dereassign.from_notebook_node(nb) + assert "blah" in output_dereassign + exporter_dereassign.raw_template = raw_template.replace("blah", "baz") + output_dereassign, _ = exporter_dereassign.from_notebook_node(nb) + assert "baz" in output_dereassign + exporter_dereassign.raw_template = '' + assert exporter_dereassign.template_file == 'rst.tpl' + output_dereassign, _ = exporter_dereassign.from_notebook_node(nb) + assert "blah" not in output_dereassign + def test_fail_to_find_template_file(self): # Create exporter with invalid template file, check that it doesn't # exist in the environment, try to convert empty notebook. Failure is From 8b95c29065c12d83658cab3cc13080ab52897922 Mon Sep 17 00:00:00 2001 From: M Pacer Date: Mon, 18 Sep 2017 19:22:12 -0700 Subject: [PATCH 029/671] remove unnecessary _load_raw_template method --- nbconvert/exporters/templateexporter.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/nbconvert/exporters/templateexporter.py b/nbconvert/exporters/templateexporter.py index a3e2056d9..d1d8e9fdf 100644 --- a/nbconvert/exporters/templateexporter.py +++ b/nbconvert/exporters/templateexporter.py @@ -164,12 +164,6 @@ def _template_file_changed(self, change): def _template_file_default(self): return self.default_template - def _load_raw_template(self, name): - if name == self._raw_template_key: - return self.raw_template, None, False - else: - return None - @observe('raw_template') def _raw_template_changed(self, change): if not change['new']: From 05934d85a5c5aa23f2d16424ef65308392cf502d Mon Sep 17 00:00:00 2001 From: oscar6echo Date: Sat, 23 Sep 2017 21:37:37 +0200 Subject: [PATCH 030/671] Update notebook CSS from 4.3.0 to 5.1.0 A key benefit if the better pandas dataframe styling. Generally I understand from @takluyver that there is no reason to use and older version. See issue #679. --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 872dde9b4..9b00fbff7 100644 --- a/setup.py +++ b/setup.py @@ -66,7 +66,7 @@ } -notebook_css_version = '4.3.0' +notebook_css_version = '5.1.0' css_url = "https://cdn.jupyter.org/notebook/%s/style/style.min.css" % notebook_css_version class FetchCSS(Command): From feb115d3076592af517543434f377129e59c75bc Mon Sep 17 00:00:00 2001 From: M Pacer Date: Mon, 2 Oct 2017 16:01:44 -0700 Subject: [PATCH 031/671] add raises-exception as a tag that allows a cell to raise an error without aborting execution by default --- nbconvert/preprocessors/execute.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/nbconvert/preprocessors/execute.py b/nbconvert/preprocessors/execute.py index 854bb23d0..1e3893b11 100644 --- a/nbconvert/preprocessors/execute.py +++ b/nbconvert/preprocessors/execute.py @@ -280,7 +280,9 @@ def preprocess_cell(self, cell, resources, cell_index): reply, outputs = self.run_cell(cell, cell_index) cell.outputs = outputs - if not self.allow_errors: + cell_allows_errors = (self.allow_errors or "raises-exception" + in cell.metadata.get("tags", [])) + for out in outputs: if out.output_type == 'error': raise CellExecutionError.from_cell_and_msg(cell, out) @@ -302,7 +304,7 @@ def _update_display_id(self, display_id, msg): except ValueError: self.log.error("unhandled iopub msg: " + msg['msg_type']) return - + for cell_idx, output_indices in self._display_id_map[display_id].items(): cell = self.nb['cells'][cell_idx] outputs = cell['outputs'] @@ -391,7 +393,7 @@ def run_cell(self, cell, cell_index=0): continue elif msg_type.startswith('comm'): continue - + display_id = None if msg_type in {'execute_result', 'display_data', 'update_display_data'}: display_id = msg['content'].get('transient', {}).get('display_id', None) @@ -420,10 +422,10 @@ def run_cell(self, cell, cell_index=0): def executenb(nb, cwd=None, **kwargs): """Execute a notebook's code, updating outputs within the notebook object. - + This is a convenient wrapper around ExecutePreprocessor. It returns the modified notebook object. - + Parameters ---------- nb : NotebookNode From 6fd43c242c46b77e96a07a67f59c61fd42e9d597 Mon Sep 17 00:00:00 2001 From: M Pacer Date: Mon, 2 Oct 2017 16:03:14 -0700 Subject: [PATCH 032/671] Adds force_raise_error to override default on cells with a raises_exception tag --- nbconvert/preprocessors/execute.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/nbconvert/preprocessors/execute.py b/nbconvert/preprocessors/execute.py index 1e3893b11..08b6f5356 100644 --- a/nbconvert/preprocessors/execute.py +++ b/nbconvert/preprocessors/execute.py @@ -130,6 +130,17 @@ class ExecutePreprocessor(Preprocessor): ) ).tag(config=True) + force_raise_errors = Bool(False, + help=dedent( + """ + If `False` (default), it does nothing. + If `True` this overrides `allow_errors` and cells tagged with + `raises-exception`, as a consequence execution is stopped and a + `CellExecutionError` is raised. + """ + ) + ).tag(config=True) + extra_arguments = List(Unicode()) kernel_name = Unicode('', @@ -226,7 +237,7 @@ def preprocess(self, nb, resources): path = resources.get('metadata', {}).get('path', '') if path == '': path = None - + # clear display_id map self._display_id_map = {} @@ -283,6 +294,7 @@ def preprocess_cell(self, cell, resources, cell_index): cell_allows_errors = (self.allow_errors or "raises-exception" in cell.metadata.get("tags", [])) + if self.force_raise_errors or not cell_allows_errors: for out in outputs: if out.output_type == 'error': raise CellExecutionError.from_cell_and_msg(cell, out) From f33523a0ccbc1fa9f7d6bd1ac4a375fb9970be41 Mon Sep 17 00:00:00 2001 From: M Pacer Date: Mon, 2 Oct 2017 16:05:17 -0700 Subject: [PATCH 033/671] add test to check that an error is raised if force_raise_errors==True --- .../Skip Exceptions with Cell Tags.ipynb | 69 +++++++++++++++++++ nbconvert/preprocessors/tests/test_execute.py | 20 +++++- 2 files changed, 87 insertions(+), 2 deletions(-) create mode 100644 nbconvert/preprocessors/tests/files/Skip Exceptions with Cell Tags.ipynb diff --git a/nbconvert/preprocessors/tests/files/Skip Exceptions with Cell Tags.ipynb b/nbconvert/preprocessors/tests/files/Skip Exceptions with Cell Tags.ipynb new file mode 100644 index 000000000..690fa3dee --- /dev/null +++ b/nbconvert/preprocessors/tests/files/Skip Exceptions with Cell Tags.ipynb @@ -0,0 +1,69 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "metadata": { + "tags": [ + "raises-exception" + ] + }, + "outputs": [ + { + "ename": "Exception", + "evalue": "message", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mException\u001b[0m Traceback (most recent call last)", + "\u001b[0;32m\u001b[0m in \u001b[0;36m\u001b[0;34m()\u001b[0m\n\u001b[1;32m 1\u001b[0m \u001b[0;31m# üñîçø∂é\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m----> 2\u001b[0;31m \u001b[0;32mraise\u001b[0m \u001b[0mException\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m\"message\"\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m", + "\u001b[0;31mException\u001b[0m: message" + ] + } + ], + "source": [ + "# üñîçø∂é\n", + "raise Exception(\"message\")" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "ok\n" + ] + } + ], + "source": [ + "print('ok')" + ] + } + ], + "metadata": { + "celltoolbar": "Tags", + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.6.2" + } + }, + "nbformat": 4, + "nbformat_minor": 1 +} diff --git a/nbconvert/preprocessors/tests/test_execute.py b/nbconvert/preprocessors/tests/test_execute.py index 0f942b75c..c2cdd750c 100644 --- a/nbconvert/preprocessors/tests/test_execute.py +++ b/nbconvert/preprocessors/tests/test_execute.py @@ -36,7 +36,7 @@ def _normalize_base64(b64_text): return b64encode(b64decode(b64_text.encode('ascii'))).decode('ascii') except (ValueError, TypeError): return b64_text - + class TestExecute(PreprocessorTestsBase): """Contains test functions for execute.py""" maxDiff = None @@ -195,7 +195,6 @@ def timeout_func(source): def test_allow_errors(self): """ - Check that conversion continues if ``allow_errors`` is False. """ current_dir = os.path.dirname(__file__) filename = os.path.join(current_dir, 'files', 'Skip Exceptions.ipynb') @@ -209,6 +208,23 @@ def test_allow_errors(self): else: assert u"# üñîçø∂é".encode('utf8', 'replace') in str(exc.value) + def test_raises_exception_cell_tag(self): + """ + Check that conversion continues if ``cell_tags`` is False. + """ + current_dir = os.path.dirname(__file__) + filename = os.path.join(current_dir, 'files', + 'Skip Exceptions with Cell Tags.ipynb') + res = self.build_resources() + res['metadata']['path'] = os.path.dirname(filename) + with pytest.raises(CellExecutionError) as exc: + self.run_notebook(filename, dict(force_raise_errors=True), res) + self.assertIsInstance(str(exc.value), str) + if sys.version_info >= (3, 0): + assert u"# üñîçø∂é" in str(exc.value) + else: + assert u"# üñîçø∂é".encode('utf8', 'replace') in str(exc.value) + def test_custom_kernel_manager(self): from .fake_kernelmanager import FakeCustomKernelManager From 7981197a879761019ce2527e7072caea39c6dc01 Mon Sep 17 00:00:00 2001 From: M Pacer Date: Mon, 2 Oct 2017 16:05:41 -0700 Subject: [PATCH 034/671] fix test docstring and indentation --- nbconvert/preprocessors/tests/test_execute.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/nbconvert/preprocessors/tests/test_execute.py b/nbconvert/preprocessors/tests/test_execute.py index c2cdd750c..93130f5c1 100644 --- a/nbconvert/preprocessors/tests/test_execute.py +++ b/nbconvert/preprocessors/tests/test_execute.py @@ -195,6 +195,7 @@ def timeout_func(source): def test_allow_errors(self): """ + Check that conversion halts if ``allow_errors`` is False. """ current_dir = os.path.dirname(__file__) filename = os.path.join(current_dir, 'files', 'Skip Exceptions.ipynb') @@ -202,11 +203,11 @@ def test_allow_errors(self): res['metadata']['path'] = os.path.dirname(filename) with pytest.raises(CellExecutionError) as exc: self.run_notebook(filename, dict(allow_errors=False), res) - self.assertIsInstance(str(exc.value), str) - if sys.version_info >= (3, 0): - assert u"# üñîçø∂é" in str(exc.value) - else: - assert u"# üñîçø∂é".encode('utf8', 'replace') in str(exc.value) + self.assertIsInstance(str(exc.value), str) + if sys.version_info >= (3, 0): + assert u"# üñîçø∂é" in str(exc.value) + else: + assert u"# üñîçø∂é".encode('utf8', 'replace') in str(exc.value) def test_raises_exception_cell_tag(self): """ From 5a7a1efe18c4b879f781eb29af0a8cffc2f9ab9b Mon Sep 17 00:00:00 2001 From: M Pacer Date: Mon, 2 Oct 2017 16:57:27 -0700 Subject: [PATCH 035/671] Add check for the existence of a filename in output metadata for assigning to file --- nbconvert/preprocessors/extractoutput.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/nbconvert/preprocessors/extractoutput.py b/nbconvert/preprocessors/extractoutput.py index 6253c06de..3d906c0c2 100755 --- a/nbconvert/preprocessors/extractoutput.py +++ b/nbconvert/preprocessors/extractoutput.py @@ -86,8 +86,10 @@ def preprocess_cell(self, cell, resources, cell_index): ext = guess_extension_without_jpe(mime_type) if ext is None: ext = '.' + mime_type.rsplit('/')[-1] - - filename = self.output_filename_template.format( + if out.metadata.get('filename', ''): + filename = out.metadata['filename'] + else: + filename = self.output_filename_template.format( unique_key=unique_key, cell_index=cell_index, index=index, From 2739afecf70514042c6373ec2631f1f139363dfe Mon Sep 17 00:00:00 2001 From: M Pacer Date: Mon, 2 Oct 2017 17:02:23 -0700 Subject: [PATCH 036/671] Add extension for file if not already present in filename --- nbconvert/preprocessors/extractoutput.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/nbconvert/preprocessors/extractoutput.py b/nbconvert/preprocessors/extractoutput.py index 3d906c0c2..c6c07104d 100755 --- a/nbconvert/preprocessors/extractoutput.py +++ b/nbconvert/preprocessors/extractoutput.py @@ -88,6 +88,8 @@ def preprocess_cell(self, cell, resources, cell_index): ext = '.' + mime_type.rsplit('/')[-1] if out.metadata.get('filename', ''): filename = out.metadata['filename'] + if not filename.endswith(ext): + filename+=ext else: filename = self.output_filename_template.format( unique_key=unique_key, From 6193ceb8ddf2fea591f862c8f003092b45e5745c Mon Sep 17 00:00:00 2001 From: M Pacer Date: Tue, 3 Oct 2017 19:03:00 -0700 Subject: [PATCH 037/671] add explicit argument to use python as a kernel, not nb.metadata.kernelspec.name --- nbconvert/preprocessors/tests/test_execute.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/nbconvert/preprocessors/tests/test_execute.py b/nbconvert/preprocessors/tests/test_execute.py index 93130f5c1..026b1c346 100644 --- a/nbconvert/preprocessors/tests/test_execute.py +++ b/nbconvert/preprocessors/tests/test_execute.py @@ -122,6 +122,9 @@ def run_notebook(self, filename, opts, resources): def test_run_notebooks(self): """Runs a series of test notebooks and compares them to their actual output""" input_files = glob.glob(os.path.join(current_dir, 'files', '*.ipynb')) + if sys.version_info >= (3, 0): + raise RuntimeError + shared_opts = dict(kernel_name="python") for filename in input_files: if os.path.basename(filename) == "Disable Stdin.ipynb": continue @@ -133,6 +136,7 @@ def test_run_notebooks(self): opts = dict() res = self.build_resources() res['metadata']['path'] = os.path.dirname(filename) + opts.update(shared_opts) input_nb, output_nb = self.run_notebook(filename, opts, res) self.assert_notebooks_equal(input_nb, output_nb) From 9c0695a1923ae460af43c93dfd1f5c774f407718 Mon Sep 17 00:00:00 2001 From: M Pacer Date: Tue, 3 Oct 2017 20:13:47 -0700 Subject: [PATCH 038/671] remove unnecessary metadata from new file --- .../Skip Exceptions with Cell Tags.ipynb | 21 +------------------ nbconvert/preprocessors/tests/test_execute.py | 2 -- 2 files changed, 1 insertion(+), 22 deletions(-) diff --git a/nbconvert/preprocessors/tests/files/Skip Exceptions with Cell Tags.ipynb b/nbconvert/preprocessors/tests/files/Skip Exceptions with Cell Tags.ipynb index 690fa3dee..aeba4a2c6 100644 --- a/nbconvert/preprocessors/tests/files/Skip Exceptions with Cell Tags.ipynb +++ b/nbconvert/preprocessors/tests/files/Skip Exceptions with Cell Tags.ipynb @@ -44,26 +44,7 @@ ] } ], - "metadata": { - "celltoolbar": "Tags", - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.6.2" - } - }, + "metadata": {}, "nbformat": 4, "nbformat_minor": 1 } diff --git a/nbconvert/preprocessors/tests/test_execute.py b/nbconvert/preprocessors/tests/test_execute.py index 026b1c346..b23abf72c 100644 --- a/nbconvert/preprocessors/tests/test_execute.py +++ b/nbconvert/preprocessors/tests/test_execute.py @@ -122,8 +122,6 @@ def run_notebook(self, filename, opts, resources): def test_run_notebooks(self): """Runs a series of test notebooks and compares them to their actual output""" input_files = glob.glob(os.path.join(current_dir, 'files', '*.ipynb')) - if sys.version_info >= (3, 0): - raise RuntimeError shared_opts = dict(kernel_name="python") for filename in input_files: if os.path.basename(filename) == "Disable Stdin.ipynb": From 6204f961764f1ecb8838e5d98eaa33ec5eb082af Mon Sep 17 00:00:00 2001 From: Tyler Makaro Date: Sat, 7 Oct 2017 13:23:50 -0700 Subject: [PATCH 039/671] Added MathJax compatibility definitions Added definitions for \TeX and \LaTeX similar to those that MathJax uses to allow LaTeX to properly compile these. --- nbconvert/templates/latex/base.tplx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/nbconvert/templates/latex/base.tplx b/nbconvert/templates/latex/base.tplx index 3c4e39475..650d529c1 100644 --- a/nbconvert/templates/latex/base.tplx +++ b/nbconvert/templates/latex/base.tplx @@ -100,7 +100,8 @@ This template does not define a docclass, the inheriting class must define this. \newcommand{\KeywordTok}[1]{\textcolor[rgb]{0.00,0.44,0.13}{\textbf{{#1}}}} \newcommand{\DataTypeTok}[1]{\textcolor[rgb]{0.56,0.13,0.00}{{#1}}} \newcommand{\DecValTok}[1]{\textcolor[rgb]{0.25,0.63,0.44}{{#1}}} - \newcommand{\BaseNTok}[1]{\textcolor[rgb]{0.25,0.63,0.44}{{#1}}} + +\newcommand{\BaseNTok}[1]{\textcolor[rgb]{0.25,0.63,0.44}{{#1}}} \newcommand{\FloatTok}[1]{\textcolor[rgb]{0.25,0.63,0.44}{{#1}}} \newcommand{\CharTok}[1]{\textcolor[rgb]{0.25,0.44,0.63}{{#1}}} \newcommand{\StringTok}[1]{\textcolor[rgb]{0.25,0.44,0.63}{{#1}}} @@ -138,6 +139,8 @@ This template does not define a docclass, the inheriting class must define this. % Math Jax compatability definitions \def\gt{>} \def\lt{<} + \def\TeX{\mbox{T\kern-.14em\lower.5ex\hbox{E}\kern-.115em X}} + \def\LaTeX{\mbox{L\kern-.325em\raise.21em\hbox{$\scriptstyle{A}$}\kern-.17em}\TeX} % Document parameters % Document title ((* block title -*)) From 7c85cb823fab5891c706b66e027ad7776dc3bba9 Mon Sep 17 00:00:00 2001 From: Tyler Makaro Date: Sat, 7 Oct 2017 13:29:33 -0700 Subject: [PATCH 040/671] indentation error --- nbconvert/templates/latex/base.tplx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nbconvert/templates/latex/base.tplx b/nbconvert/templates/latex/base.tplx index 650d529c1..b9b003f6b 100644 --- a/nbconvert/templates/latex/base.tplx +++ b/nbconvert/templates/latex/base.tplx @@ -101,7 +101,7 @@ This template does not define a docclass, the inheriting class must define this. \newcommand{\DataTypeTok}[1]{\textcolor[rgb]{0.56,0.13,0.00}{{#1}}} \newcommand{\DecValTok}[1]{\textcolor[rgb]{0.25,0.63,0.44}{{#1}}} -\newcommand{\BaseNTok}[1]{\textcolor[rgb]{0.25,0.63,0.44}{{#1}}} + \newcommand{\BaseNTok}[1]{\textcolor[rgb]{0.25,0.63,0.44}{{#1}}} \newcommand{\FloatTok}[1]{\textcolor[rgb]{0.25,0.63,0.44}{{#1}}} \newcommand{\CharTok}[1]{\textcolor[rgb]{0.25,0.44,0.63}{{#1}}} \newcommand{\StringTok}[1]{\textcolor[rgb]{0.25,0.44,0.63}{{#1}}} From a496b6b621b31d370a98b406b942e49ce1d5a109 Mon Sep 17 00:00:00 2001 From: Tyler Makaro Date: Sat, 7 Oct 2017 13:35:54 -0700 Subject: [PATCH 041/671] removed blank line My first commit added the extra blank line, so this fixes it. --- nbconvert/templates/latex/base.tplx | 1 - 1 file changed, 1 deletion(-) diff --git a/nbconvert/templates/latex/base.tplx b/nbconvert/templates/latex/base.tplx index b9b003f6b..3da12a5bd 100644 --- a/nbconvert/templates/latex/base.tplx +++ b/nbconvert/templates/latex/base.tplx @@ -100,7 +100,6 @@ This template does not define a docclass, the inheriting class must define this. \newcommand{\KeywordTok}[1]{\textcolor[rgb]{0.00,0.44,0.13}{\textbf{{#1}}}} \newcommand{\DataTypeTok}[1]{\textcolor[rgb]{0.56,0.13,0.00}{{#1}}} \newcommand{\DecValTok}[1]{\textcolor[rgb]{0.25,0.63,0.44}{{#1}}} - \newcommand{\BaseNTok}[1]{\textcolor[rgb]{0.25,0.63,0.44}{{#1}}} \newcommand{\FloatTok}[1]{\textcolor[rgb]{0.25,0.63,0.44}{{#1}}} \newcommand{\CharTok}[1]{\textcolor[rgb]{0.25,0.44,0.63}{{#1}}} From 1929887465a5dbbd97de137b97aa73d957a02e2f Mon Sep 17 00:00:00 2001 From: Thomas Kluyver Date: Mon, 9 Oct 2017 17:24:49 +0100 Subject: [PATCH 042/671] Use sphinxcontrib_github_alt for Github issue/PR links --- docs/environment.yml | 1 + docs/source/conf.py | 6 +- docs/sphinxext/github.py | 156 --------------------------------------- 3 files changed, 3 insertions(+), 160 deletions(-) delete mode 100644 docs/sphinxext/github.py diff --git a/docs/environment.yml b/docs/environment.yml index 7992e3db5..d70cb770b 100644 --- a/docs/environment.yml +++ b/docs/environment.yml @@ -13,3 +13,4 @@ dependencies: - entrypoints - pip: - nbsphinx>=0.2.12 + - sphinxcontrib_github_alt diff --git a/docs/source/conf.py b/docs/source/conf.py index 72246b1ba..bdb3ac188 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -14,13 +14,11 @@ # serve to show the default. import os -import sys # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. -# sys.path.insert(0, os.path.abspath('.')) -sys.path.insert(0, os.path.abspath(os.path.join('..', 'sphinxext'))) +#sys.path.insert(0, os.path.abspath('.')) if os.environ.get('READTHEDOCS', ''): # RTD doesn't use the repo's Makefile to build docs. We run @@ -47,7 +45,7 @@ 'sphinx.ext.napoleon', 'nbsphinx', 'IPython.sphinxext.ipython_console_highlighting', - 'github', + 'sphinxcontrib_github_alt', ] diff --git a/docs/sphinxext/github.py b/docs/sphinxext/github.py deleted file mode 100644 index 3de499247..000000000 --- a/docs/sphinxext/github.py +++ /dev/null @@ -1,156 +0,0 @@ -"""Define text roles for GitHub - -* ghissue - Issue -* ghpull - Pull Request -* ghuser - User - -Adapted from bitbucket example here: -https://github.com/ipython/ipython/blob/master/docs/sphinxext/github.py - -Authors -------- - -* Doug Hellmann -* M Pacer -* Min RK -""" -# -# Original Copyright (c) 2010 Doug Hellmann. All rights reserved. -# - -from docutils import nodes, utils -from docutils.parsers.rst.roles import set_classes - -def make_link_node(rawtext, app, type, slug, options): - """Create a link to a github resource. - - :param rawtext: Text being replaced with link node. - :param app: Sphinx application context - :param type: Link type (issues, changeset, etc.) - :param slug: ID of the thing to link to - :param options: Options dictionary passed to role func. - """ - - try: - base = app.config.github_project_url - if not base: - raise AttributeError - if not base.endswith('/'): - base += '/' - except AttributeError as err: - raise ValueError('github_project_url configuration value is not set (%s)' % str(err)) - - ref = base + type + '/' + slug + '/' - set_classes(options) - prefix = "#" - node = nodes.reference(rawtext, prefix + utils.unescape(slug), refuri=ref, - **options) - return node - -def ghissue_role(name, rawtext, text, lineno, inliner, options={}, content=[]): - """Link to a GitHub issue. - - Returns 2 part tuple containing list of nodes to insert into the - document and a list of system messages. Both are allowed to be - empty. - - :param name: The role name used in the document. - :param rawtext: The entire markup snippet, with role. - :param text: The text marked with the role. - :param lineno: The line number where rawtext appears in the input. - :param inliner: The inliner instance that called us. - :param options: Directive options for customization. - :param content: The directive content for customization. - """ - - try: - issue_num = int(text) - if issue_num <= 0: - raise ValueError - except ValueError: - msg = inliner.reporter.error( - 'GitHub issue number must be a number greater than or equal to 1; ' - '"%s" is invalid.' % text, line=lineno) - prb = inliner.problematic(rawtext, rawtext, msg) - return [prb], [msg] - app = inliner.document.settings.env.app - #app.info('issue %r' % text) - if 'pull' in name.lower(): - category = 'pull' - elif 'issue' in name.lower(): - category = 'issues' - else: - msg = inliner.reporter.error( - 'GitHub roles include "ghpull" and "ghissue", ' - '"%s" is invalid.' % name, line=lineno) - prb = inliner.problematic(rawtext, rawtext, msg) - return [prb], [msg] - node = make_link_node(rawtext, app, category, str(issue_num), options) - return [node], [] - -def ghuser_role(name, rawtext, text, lineno, inliner, options={}, content=[]): - """Link to a GitHub user. - - Returns 2 part tuple containing list of nodes to insert into the - document and a list of system messages. Both are allowed to be - empty. - - :param name: The role name used in the document. - :param rawtext: The entire markup snippet, with role. - :param text: The text marked with the role. - :param lineno: The line number where rawtext appears in the input. - :param inliner: The inliner instance that called us. - :param options: Directive options for customization. - :param content: The directive content for customization. - """ - app = inliner.document.settings.env.app - # app.info('user link %r' % text) - ref = 'https://www.github.com/' + text - node = nodes.reference(rawtext, text, refuri=ref, **options) - return [node], [] - -def ghcommit_role(name, rawtext, text, lineno, inliner, options={}, content=[]): - """Link to a GitHub commit. - - Returns 2 part tuple containing list of nodes to insert into the - document and a list of system messages. Both are allowed to be - empty. - - :param name: The role name used in the document. - :param rawtext: The entire markup snippet, with role. - :param text: The text marked with the role. - :param lineno: The line number where rawtext appears in the input. - :param inliner: The inliner instance that called us. - :param options: Directive options for customization. - :param content: The directive content for customization. - """ - app = inliner.document.settings.env.app - #app.info('user link %r' % text) - try: - base = app.config.github_project_url - if not base: - raise AttributeError - if not base.endswith('/'): - base += '/' - except AttributeError as err: - raise ValueError('github_project_url configuration value is not set (%s)' % str(err)) - - ref = base + text - node = nodes.reference(rawtext, text[:6], refuri=ref, **options) - return [node], [] - - -def setup(app): - """Install the plugin. - - :param app: Sphinx application context. - """ - app.info('Initializing GitHub plugin') - app.add_role('ghissue', ghissue_role) - app.add_role('ghpull', ghissue_role) - app.add_role('ghuser', ghuser_role) - app.add_role('ghcommit', ghcommit_role) - app.add_config_value('github_project_url', None, 'env') - - metadata = {'parallel_read_safe': True, 'parallel_write_safe': True} - return metadata From 1040f0af2b9c43dea78a365e9bdaa7c83d8f8f8c Mon Sep 17 00:00:00 2001 From: M Pacer Date: Thu, 12 Oct 2017 15:53:30 -0700 Subject: [PATCH 043/671] better docstrings and help messages --- nbconvert/preprocessors/execute.py | 13 +++++++++---- nbconvert/preprocessors/tests/test_execute.py | 3 ++- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/nbconvert/preprocessors/execute.py b/nbconvert/preprocessors/execute.py index 08b6f5356..f5cca4280 100644 --- a/nbconvert/preprocessors/execute.py +++ b/nbconvert/preprocessors/execute.py @@ -133,10 +133,15 @@ class ExecutePreprocessor(Preprocessor): force_raise_errors = Bool(False, help=dedent( """ - If `False` (default), it does nothing. - If `True` this overrides `allow_errors` and cells tagged with - `raises-exception`, as a consequence execution is stopped and a - `CellExecutionError` is raised. + If False (default), errors from executing the notebook can be + allowed with a `raises-exception` tag on a single cell, or the + `allow_errors` configurable option for all cells. An allowed error + will be recorded in notebook output, and execution will continue. + If an error occurs when it is not explicitly allowed, a + `CellExecutionError` will be raised. + If True, `CellExecutionError` will be raised for any error that occurs + while executing the notebook. This overrides both the + `allow_errors` option and the `raises-exception` cell tag. """ ) ).tag(config=True) diff --git a/nbconvert/preprocessors/tests/test_execute.py b/nbconvert/preprocessors/tests/test_execute.py index b23abf72c..38edd7157 100644 --- a/nbconvert/preprocessors/tests/test_execute.py +++ b/nbconvert/preprocessors/tests/test_execute.py @@ -213,7 +213,8 @@ def test_allow_errors(self): def test_raises_exception_cell_tag(self): """ - Check that conversion continues if ``cell_tags`` is False. + Check that conversion continues if ``raises-exception`` is present in + the cell in which an exception is raised. """ current_dir = os.path.dirname(__file__) filename = os.path.join(current_dir, 'files', From 36737af84a7d08a806c9cb5a82b7094b96ab0c0f Mon Sep 17 00:00:00 2001 From: M Pacer Date: Thu, 12 Oct 2017 16:17:10 -0700 Subject: [PATCH 044/671] throw error if filename is not unique --- nbconvert/preprocessors/extractoutput.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/nbconvert/preprocessors/extractoutput.py b/nbconvert/preprocessors/extractoutput.py index c6c07104d..74948833b 100755 --- a/nbconvert/preprocessors/extractoutput.py +++ b/nbconvert/preprocessors/extractoutput.py @@ -106,6 +106,13 @@ def preprocess_cell(self, cell, resources, cell_index): out.metadata.setdefault('filenames', {}) out.metadata['filenames'][mime_type] = filename + if filename in resources['outputs']: + raise ValueError( + "Your filename: {} appears more than once. " + "Filenames must be unique across the notebook. The " + "second time this filename appeared was in cell " + "{}.".format(filename, cell_index) + ) #In the resources, make the figure available via # resources['outputs']['filename'] = data resources['outputs'][filename] = data From 69285de43df5b96859c7784f97f70e910ee74c21 Mon Sep 17 00:00:00 2001 From: Tyler Makaro Date: Tue, 17 Oct 2017 15:20:32 -0700 Subject: [PATCH 045/671] Updated compatibility definitions These new definitions allow for the original LaTeX and TeX logos inside of math mode. --- nbconvert/templates/latex/base.tplx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/nbconvert/templates/latex/base.tplx b/nbconvert/templates/latex/base.tplx index 3da12a5bd..222ea40f4 100644 --- a/nbconvert/templates/latex/base.tplx +++ b/nbconvert/templates/latex/base.tplx @@ -138,8 +138,10 @@ This template does not define a docclass, the inheriting class must define this. % Math Jax compatability definitions \def\gt{>} \def\lt{<} - \def\TeX{\mbox{T\kern-.14em\lower.5ex\hbox{E}\kern-.115em X}} - \def\LaTeX{\mbox{L\kern-.325em\raise.21em\hbox{$\scriptstyle{A}$}\kern-.17em}\TeX} + \let\Oldtex\TeX + \let\Oldlatex\LaTeX + \renewcommand{\TeX}{\textrm{\Oldtex}} + \renewcommand{\LaTeX}{\textrm{\Oldlatex}} % Document parameters % Document title ((* block title -*)) From bfe96613eaf0d77114915660bf774c1e0e206b58 Mon Sep 17 00:00:00 2001 From: M Pacer Date: Tue, 17 Oct 2017 15:49:27 -0700 Subject: [PATCH 046/671] Switch test to talk about force_raise_errors specifically --- nbconvert/preprocessors/tests/test_execute.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/nbconvert/preprocessors/tests/test_execute.py b/nbconvert/preprocessors/tests/test_execute.py index 38edd7157..f6657fff9 100644 --- a/nbconvert/preprocessors/tests/test_execute.py +++ b/nbconvert/preprocessors/tests/test_execute.py @@ -211,10 +211,10 @@ def test_allow_errors(self): else: assert u"# üñîçø∂é".encode('utf8', 'replace') in str(exc.value) - def test_raises_exception_cell_tag(self): + def test_force_raise_errors(self): """ - Check that conversion continues if ``raises-exception`` is present in - the cell in which an exception is raised. + Check that conversion halts if the ``force_raise_errors`` traitlet on + ExecutePreprocessor is set to True. """ current_dir = os.path.dirname(__file__) filename = os.path.join(current_dir, 'files', From 2e47956bb626eac5f2d42dc11aaa2fcf772526d7 Mon Sep 17 00:00:00 2001 From: geniusupgrader Date: Sun, 22 Oct 2017 15:48:28 +0200 Subject: [PATCH 047/671] added shebang for python --- nbconvert/templates/python.tpl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nbconvert/templates/python.tpl b/nbconvert/templates/python.tpl index 112f39b95..caaff6d4c 100644 --- a/nbconvert/templates/python.tpl +++ b/nbconvert/templates/python.tpl @@ -1,6 +1,6 @@ {%- extends 'null.tpl' -%} -{% block header %} +{% block header %}#!/usr/bin/env python # coding: utf-8 {% endblock header %} From f70805825bdad5a55d4bcc168dae965124a3bc81 Mon Sep 17 00:00:00 2001 From: Josh Barnes Date: Sun, 22 Oct 2017 23:24:01 +0100 Subject: [PATCH 048/671] Fixes for traitlets 4.1 deprecation warnings --- nbconvert/preprocessors/regexremove.py | 2 +- nbconvert/preprocessors/tagremove.py | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/nbconvert/preprocessors/regexremove.py b/nbconvert/preprocessors/regexremove.py index b40473686..7c185df52 100644 --- a/nbconvert/preprocessors/regexremove.py +++ b/nbconvert/preprocessors/regexremove.py @@ -38,7 +38,7 @@ class RegexRemovePreprocessor(Preprocessor): documentation in python. """ - patterns = List(Unicode, default_value=[r'\Z']).tag(config=True) + patterns = List(Unicode(), default_value=[r'\Z']).tag(config=True) def check_conditions(self, cell): """ diff --git a/nbconvert/preprocessors/tagremove.py b/nbconvert/preprocessors/tagremove.py index 99869253c..8b0c5751d 100644 --- a/nbconvert/preprocessors/tagremove.py +++ b/nbconvert/preprocessors/tagremove.py @@ -25,17 +25,17 @@ class TagRemovePreprocessor(ClearOutputPreprocessor): """ - remove_cell_tags = Set(Unicode, default_value=[], + remove_cell_tags = Set(Unicode(), default_value=[], help=("Tags indicating which cells are to be removed," "matches tags in `cell.metadata.tags`.")).tag(config=True) - remove_all_outputs_tags = Set(Unicode, default_value=[], + remove_all_outputs_tags = Set(Unicode(), default_value=[], help=("Tags indicating cells for which the outputs are to be removed," "matches tags in `cell.metadata.tags`.")).tag(config=True) - remove_single_output_tags = Set(Unicode, default_value=[], + remove_single_output_tags = Set(Unicode(), default_value=[], help=("Tags indicating which individual outputs are to be removed," "matches output *i* tags in `cell.outputs[i].metadata.tags`.") ).tag(config=True) - remove_input_tags = Set(Unicode, default_value=[], + remove_input_tags = Set(Unicode(), default_value=[], help=("Tags indicating cells for which input is to be removed," "matches tags in `cell.metadata.tags`.")).tag(config=True) From 275ef1b5680f61efad9073ef250c91cd7092a518 Mon Sep 17 00:00:00 2001 From: geniusupgrader Date: Mon, 23 Oct 2017 16:04:51 +0200 Subject: [PATCH 049/671] add shebang to python, using dashes better with dashes --- nbconvert/templates/python.tpl | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/nbconvert/templates/python.tpl b/nbconvert/templates/python.tpl index caaff6d4c..eed2f923f 100644 --- a/nbconvert/templates/python.tpl +++ b/nbconvert/templates/python.tpl @@ -1,6 +1,7 @@ {%- extends 'null.tpl' -%} -{% block header %}#!/usr/bin/env python +{%- block header -%} +#!/usr/bin/env python # coding: utf-8 {% endblock header %} From 349fc892dafb22137b71bbd0dd05ac7c7c5cd329 Mon Sep 17 00:00:00 2001 From: M Pacer Date: Wed, 25 Oct 2017 15:59:46 -0700 Subject: [PATCH 050/671] add shebang for ipython, not python; add test for functionality --- nbconvert/exporters/tests/test_python.py | 1 + nbconvert/templates/python.tpl | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/nbconvert/exporters/tests/test_python.py b/nbconvert/exporters/tests/test_python.py index 36ee0e9f3..75271fa9b 100644 --- a/nbconvert/exporters/tests/test_python.py +++ b/nbconvert/exporters/tests/test_python.py @@ -21,3 +21,4 @@ def test_export(self): """Can a PythonExporter export something?""" (output, resources) = self.exporter_class().from_filename(self._get_notebook()) self.assertIn("coding: utf-8", output) + self.assertIn("#!/usr/bin/env ipython", output) diff --git a/nbconvert/templates/python.tpl b/nbconvert/templates/python.tpl index eed2f923f..41719b80e 100644 --- a/nbconvert/templates/python.tpl +++ b/nbconvert/templates/python.tpl @@ -1,7 +1,7 @@ {%- extends 'null.tpl' -%} {%- block header -%} -#!/usr/bin/env python +#!/usr/bin/env ipython # coding: utf-8 {% endblock header %} From 08aa66fd8889aa156fa3d2a6ad06875293697c0f Mon Sep 17 00:00:00 2001 From: Marco Rossi Date: Sat, 11 Nov 2017 16:10:22 +0100 Subject: [PATCH 051/671] Use "title" instead of "name" for metadata to match the notebook format --- nbconvert/exporters/exporter.py | 6 +++--- nbconvert/templates/html/full.tpl | 2 +- nbconvert/templates/html/slides_reveal.tpl | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/nbconvert/exporters/exporter.py b/nbconvert/exporters/exporter.py index e70868f91..2daedf861 100644 --- a/nbconvert/exporters/exporter.py +++ b/nbconvert/exporters/exporter.py @@ -164,7 +164,7 @@ def from_filename(self, filename, resources=None, **kw): resources['metadata'] = ResourcesDict() path, basename = os.path.split(filename) notebook_name = basename[:basename.rfind('.')] - resources['metadata']['name'] = notebook_name + resources['metadata']['title'] = notebook_name resources['metadata']['path'] = path modified_date = datetime.datetime.fromtimestamp(os.path.getmtime(filename)) @@ -277,8 +277,8 @@ def _init_resources(self, resources): resources['metadata'] = new_metadata else: resources['metadata'] = ResourcesDict() - if not resources['metadata']['name']: - resources['metadata']['name'] = 'Notebook' + if not resources['metadata']['title']: + resources['metadata']['title'] = 'Notebook' #Set the output extension resources['output_extension'] = self.file_extension diff --git a/nbconvert/templates/html/full.tpl b/nbconvert/templates/html/full.tpl index 786e2a420..bf336f7fd 100644 --- a/nbconvert/templates/html/full.tpl +++ b/nbconvert/templates/html/full.tpl @@ -8,7 +8,7 @@ {%- block html_head -%} -{{resources['metadata']['name']}} +{{resources['metadata']['title']}} {%- if "widgets" in nb.metadata -%} diff --git a/nbconvert/templates/html/slides_reveal.tpl b/nbconvert/templates/html/slides_reveal.tpl index bd1e06b15..fcc1959b8 100644 --- a/nbconvert/templates/html/slides_reveal.tpl +++ b/nbconvert/templates/html/slides_reveal.tpl @@ -44,7 +44,7 @@ -{{resources['metadata']['name']}} slides +{{resources['metadata']['title']}} slides From dcd4f294c9c9137e3f1d215e2f99afb3b8346b92 Mon Sep 17 00:00:00 2001 From: M Pacer Date: Mon, 13 Nov 2017 13:10:08 -0800 Subject: [PATCH 052/671] upgrade mistune dependency --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 9b00fbff7..86f7bd901 100644 --- a/setup.py +++ b/setup.py @@ -185,7 +185,7 @@ def run(self): setuptools_args = {} install_requires = setuptools_args['install_requires'] = [ - 'mistune>=0.7.4', + 'mistune>=0.8.1', 'jinja2', 'pygments', 'traitlets>=4.2', From ea4f51050ce05c2b2466343b9ea1619cbdc00546 Mon Sep 17 00:00:00 2001 From: M Pacer Date: Mon, 13 Nov 2017 13:19:39 -0800 Subject: [PATCH 053/671] switch command in shebang to python instead of ipython --- nbconvert/exporters/tests/test_python.py | 2 +- nbconvert/templates/python.tpl | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/nbconvert/exporters/tests/test_python.py b/nbconvert/exporters/tests/test_python.py index 75271fa9b..9fc5aef75 100644 --- a/nbconvert/exporters/tests/test_python.py +++ b/nbconvert/exporters/tests/test_python.py @@ -21,4 +21,4 @@ def test_export(self): """Can a PythonExporter export something?""" (output, resources) = self.exporter_class().from_filename(self._get_notebook()) self.assertIn("coding: utf-8", output) - self.assertIn("#!/usr/bin/env ipython", output) + self.assertIn("#!/usr/bin/env python", output) diff --git a/nbconvert/templates/python.tpl b/nbconvert/templates/python.tpl index 41719b80e..eed2f923f 100644 --- a/nbconvert/templates/python.tpl +++ b/nbconvert/templates/python.tpl @@ -1,7 +1,7 @@ {%- extends 'null.tpl' -%} {%- block header -%} -#!/usr/bin/env ipython +#!/usr/bin/env python # coding: utf-8 {% endblock header %} From bd20c4f6959d277a9e84cf8f48456e57268aeac4 Mon Sep 17 00:00:00 2001 From: M Pacer Date: Mon, 13 Nov 2017 13:39:34 -0800 Subject: [PATCH 054/671] clarify error message per @takluyver's suggestions --- nbconvert/preprocessors/extractoutput.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/nbconvert/preprocessors/extractoutput.py b/nbconvert/preprocessors/extractoutput.py index 74948833b..e0e773684 100755 --- a/nbconvert/preprocessors/extractoutput.py +++ b/nbconvert/preprocessors/extractoutput.py @@ -108,9 +108,13 @@ def preprocess_cell(self, cell, resources, cell_index): if filename in resources['outputs']: raise ValueError( - "Your filename: {} appears more than once. " - "Filenames must be unique across the notebook. The " - "second time this filename appeared was in cell " + "Your outputs have filename metadata associated " + "with them. Nbconvert saves these outputs to " + "external files using this filename metadata. " + "Filenames need to be unique across the notebook, " + "or images will be overwritten. The filename {} is " + "associated with more than one output. The second " + "output associated with this filename is in cell " "{}.".format(filename, cell_index) ) #In the resources, make the figure available via From 5cb50ee30b0a5930891f803ca39edca5280b2539 Mon Sep 17 00:00:00 2001 From: Thomas Kluyver Date: Fri, 17 Nov 2017 13:24:55 +0000 Subject: [PATCH 055/671] Use defusedxml to parse potentially untrusted XML Closes gh-706 --- nbconvert/filters/strings.py | 7 +++++-- setup.py | 1 + 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/nbconvert/filters/strings.py b/nbconvert/filters/strings.py index acdec5706..e5bad48ac 100755 --- a/nbconvert/filters/strings.py +++ b/nbconvert/filters/strings.py @@ -17,7 +17,10 @@ from urllib.parse import quote # Py 3 except ImportError: from urllib2 import quote # Py 2 -from xml.etree import ElementTree + +# defusedxml does safe(r) parsing of untrusted XML data +from defusedxml import cElementTree as ElementTree +from xml.etree.cElementTree import Element from ipython_genutils import py3compat @@ -98,7 +101,7 @@ def add_anchor(html, anchor_link_text=u'¶'): return html link = _convert_header_id(html2text(h)) h.set('id', link) - a = ElementTree.Element("a", {"class" : "anchor-link", "href" : "#" + link}) + a = Element("a", {"class" : "anchor-link", "href" : "#" + link}) a.text = anchor_link_text h.append(a) diff --git a/setup.py b/setup.py index 86f7bd901..8fb3f373f 100644 --- a/setup.py +++ b/setup.py @@ -195,6 +195,7 @@ def run(self): 'bleach', 'pandocfilters>=1.4.1', 'testpath', + 'defusedxml', ] extra_requirements = { From 8944e453381cc2b8df1e95e32d2bd729ea8f13ba Mon Sep 17 00:00:00 2001 From: Thomas Kluyver Date: Mon, 27 Nov 2017 12:01:41 +0000 Subject: [PATCH 056/671] Linkify PR number --- docs/source/changelog.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst index 11b8b610c..73c9b2714 100644 --- a/docs/source/changelog.rst +++ b/docs/source/changelog.rst @@ -8,7 +8,7 @@ Changes in nbconvert ----- `5.3.1 on Github `__ -- MANIFEST.in updated to include ``LICENSE`` and ``scripts/`` when creating sdist. #666 +- MANIFEST.in updated to include ``LICENSE`` and ``scripts/`` when creating sdist. :ghpull:`666` 5.3 --- From a2a7215d809ffe46719d4385651418964b1d9e8b Mon Sep 17 00:00:00 2001 From: Marco Rossi Date: Sun, 3 Dec 2017 11:15:31 +0100 Subject: [PATCH 057/671] Revert "Use "title" instead of "name" for metadata to match the notebook format" This reverts commit 08aa66fd8889aa156fa3d2a6ad06875293697c0f. --- nbconvert/exporters/exporter.py | 6 +++--- nbconvert/templates/html/full.tpl | 2 +- nbconvert/templates/html/slides_reveal.tpl | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/nbconvert/exporters/exporter.py b/nbconvert/exporters/exporter.py index 2daedf861..e70868f91 100644 --- a/nbconvert/exporters/exporter.py +++ b/nbconvert/exporters/exporter.py @@ -164,7 +164,7 @@ def from_filename(self, filename, resources=None, **kw): resources['metadata'] = ResourcesDict() path, basename = os.path.split(filename) notebook_name = basename[:basename.rfind('.')] - resources['metadata']['title'] = notebook_name + resources['metadata']['name'] = notebook_name resources['metadata']['path'] = path modified_date = datetime.datetime.fromtimestamp(os.path.getmtime(filename)) @@ -277,8 +277,8 @@ def _init_resources(self, resources): resources['metadata'] = new_metadata else: resources['metadata'] = ResourcesDict() - if not resources['metadata']['title']: - resources['metadata']['title'] = 'Notebook' + if not resources['metadata']['name']: + resources['metadata']['name'] = 'Notebook' #Set the output extension resources['output_extension'] = self.file_extension diff --git a/nbconvert/templates/html/full.tpl b/nbconvert/templates/html/full.tpl index bf336f7fd..786e2a420 100644 --- a/nbconvert/templates/html/full.tpl +++ b/nbconvert/templates/html/full.tpl @@ -8,7 +8,7 @@ {%- block html_head -%} -{{resources['metadata']['title']}} +{{resources['metadata']['name']}} {%- if "widgets" in nb.metadata -%} diff --git a/nbconvert/templates/html/slides_reveal.tpl b/nbconvert/templates/html/slides_reveal.tpl index fcc1959b8..bd1e06b15 100644 --- a/nbconvert/templates/html/slides_reveal.tpl +++ b/nbconvert/templates/html/slides_reveal.tpl @@ -44,7 +44,7 @@ -{{resources['metadata']['title']}} slides +{{resources['metadata']['name']}} slides From bd486c3d001c776c4af0b114192c02885a7a9f38 Mon Sep 17 00:00:00 2001 From: Marco Rossi Date: Sun, 3 Dec 2017 12:22:33 +0100 Subject: [PATCH 058/671] Add use title in html templates if it is set in metadata --- nbconvert/templates/html/full.tpl | 4 ++++ nbconvert/templates/html/slides_reveal.tpl | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/nbconvert/templates/html/full.tpl b/nbconvert/templates/html/full.tpl index 786e2a420..978e47c84 100644 --- a/nbconvert/templates/html/full.tpl +++ b/nbconvert/templates/html/full.tpl @@ -8,7 +8,11 @@ {%- block html_head -%} +{%- if 'title' in resources['metadata'] -%} +{{resources['metadata']['title']}} +{%- else -%} {{resources['metadata']['name']}} +{%- endif -%} {%- if "widgets" in nb.metadata -%} diff --git a/nbconvert/templates/html/slides_reveal.tpl b/nbconvert/templates/html/slides_reveal.tpl index bd1e06b15..f755acfe0 100644 --- a/nbconvert/templates/html/slides_reveal.tpl +++ b/nbconvert/templates/html/slides_reveal.tpl @@ -44,7 +44,11 @@ +{%- if 'title' in resources['metadata'] -%} +{{resources['metadata']['title']}} slides +{%- else -%} {{resources['metadata']['name']}} slides +{%- endif -%} From 1db966646b1e28b157ff2fa86474256dc9ff781c Mon Sep 17 00:00:00 2001 From: "Danilo J. S. Bellini" Date: Mon, 4 Dec 2017 03:36:35 -0200 Subject: [PATCH 059/671] Add Markdown block lexer/parser for LaTeX blocks The block lexer/parser was splitting equations like this $$ x = 2 $$ So the inline lexer/parser was never seeing the whole equation, and it wasn't getting properly rendered. This fixes such breaking by adding a block-level lexer/parser to the LaTeX equations written as either $$...$$ or \\[...\\] The inline "block math" parsing code was kept as is, since the above equation could have been part of a paragraph like "$$x = 2$$" to keep the compatibility with Jupyter Notebook rendering engine (and because there's a test enforcing that behavior) --- nbconvert/filters/markdown_mistune.py | 40 ++++++++++++++++++++++++ nbconvert/filters/tests/test_markdown.py | 12 +++++++ 2 files changed, 52 insertions(+) diff --git a/nbconvert/filters/markdown_mistune.py b/nbconvert/filters/markdown_mistune.py index 0b6a9eeb0..79b3795fd 100644 --- a/nbconvert/filters/markdown_mistune.py +++ b/nbconvert/filters/markdown_mistune.py @@ -21,6 +21,12 @@ from nbconvert.filters.strings import add_anchor +class MathBlockGrammar(mistune.BlockGrammar): + block_math = re.compile(r"^\$\$(.*?)\$\$|^\\\\\[(.*?)\\\\\]", re.DOTALL) + latex_environment = re.compile(r"^\\begin\{([a-z]*\*?)\}(.*?)\\end\{\1\}", + re.DOTALL) + + class MathInlineGrammar(mistune.InlineGrammar): inline_math = re.compile(r"^\$(.+?)\$|^\\\\\((.+?)\\\\\)", re.DOTALL) block_math = re.compile(r"^\$\$(.*?)\$\$|^\\\\\[(.*?)\\\\\]", re.DOTALL) @@ -29,6 +35,31 @@ class MathInlineGrammar(mistune.InlineGrammar): text = re.compile(r'^[\s\S]+?(?=[\\a;a-b<0$$", "$$$$", + """$$x += +2$$""", + r"""$$ + b_{l_1,l_2,l_3}^{\phi\phi\omega} = -8\, l_1\times l_2 \,l_1\cdot l_2 \int_0^{\chi_*} d\chi \frac{W(\chi,\chi_*)^2}{\chi^2} \int_0^{\chi}d\chi' \frac{W(\chi',\chi)W(\chi',\chi_*)}{{\chi'}^2} \left[ + P_\psi\left(\frac{l_1}{\chi},z(\chi)\right) P_\Psi\left(\frac{l_2}{\chi'},z(\chi')\right) +- (l_1\leftrightarrow l_2) +\right] +$$""", + r"""\begin{equation*} +x = 2 *55* 7 +\end{equation*}""", """$ \\begin{tabular}{ l c r } 1 & 2 & 3 \\ From 018ca575650b8f62ae4d6da5e7113d9e01bfaca0 Mon Sep 17 00:00:00 2001 From: Marco Rossi Date: Wed, 6 Dec 2017 11:54:17 +0100 Subject: [PATCH 060/671] Fix use notbook-metadata instead of resources-metadata --- nbconvert/templates/html/full.tpl | 4 ++-- nbconvert/templates/html/slides_reveal.tpl | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/nbconvert/templates/html/full.tpl b/nbconvert/templates/html/full.tpl index 978e47c84..133f427c5 100644 --- a/nbconvert/templates/html/full.tpl +++ b/nbconvert/templates/html/full.tpl @@ -8,8 +8,8 @@ {%- block html_head -%} -{%- if 'title' in resources['metadata'] -%} -{{resources['metadata']['title']}} +{%- if nb.metadata.get('title','') -%} +{{nb.metadata.get('title', '')}} {%- else -%} {{resources['metadata']['name']}} {%- endif -%} diff --git a/nbconvert/templates/html/slides_reveal.tpl b/nbconvert/templates/html/slides_reveal.tpl index f755acfe0..4fb54eb81 100644 --- a/nbconvert/templates/html/slides_reveal.tpl +++ b/nbconvert/templates/html/slides_reveal.tpl @@ -44,8 +44,8 @@ -{%- if 'title' in resources['metadata'] -%} -{{resources['metadata']['title']}} slides +{%- if nb.metadata.get('title','') -%} +{{nb.metadata.get('title', '')}} slides {%- else -%} {{resources['metadata']['name']}} slides {%- endif -%} From 542f6de2035b129de56eb1d86eee2131c21b3dbf Mon Sep 17 00:00:00 2001 From: M Pacer Date: Wed, 6 Dec 2017 09:44:59 -0800 Subject: [PATCH 061/671] use the multiline math block lexer as a pass through to the inlinelexer --- nbconvert/filters/markdown_mistune.py | 34 ++++++++++----------------- 1 file changed, 12 insertions(+), 22 deletions(-) diff --git a/nbconvert/filters/markdown_mistune.py b/nbconvert/filters/markdown_mistune.py index 79b3795fd..1d418ac4b 100644 --- a/nbconvert/filters/markdown_mistune.py +++ b/nbconvert/filters/markdown_mistune.py @@ -22,9 +22,10 @@ class MathBlockGrammar(mistune.BlockGrammar): - block_math = re.compile(r"^\$\$(.*?)\$\$|^\\\\\[(.*?)\\\\\]", re.DOTALL) - latex_environment = re.compile(r"^\\begin\{([a-z]*\*?)\}(.*?)\\end\{\1\}", - re.DOTALL) + multi_math_str = "|".join([r"(^\$\$.*?\$\$)", + r"(^\\\\\[.*?\\\\\])", + r"(^\\begin\{([a-z]*\*?)\}(.*?)\\end\{\4\})"]) + multiline_math = re.compile(multi_math_str, re.DOTALL) class MathInlineGrammar(mistune.InlineGrammar): @@ -36,7 +37,7 @@ class MathInlineGrammar(mistune.InlineGrammar): class MathBlockLexer(mistune.BlockLexer): - default_rules = (['block_math', 'latex_environment'] + default_rules = (['multiline_math'] + mistune.BlockLexer.default_rules) def __init__(self, rules=None, **kwargs): @@ -44,19 +45,11 @@ def __init__(self, rules=None, **kwargs): rules = MathBlockGrammar() super(MathBlockLexer, self).__init__(rules, **kwargs) - def parse_block_math(self, m): - """Add token for a $$math$$ block""" + def parse_multiline_math(self, m): + """Add token to pass through mutiline math.""" self.tokens.append({ - 'type': 'block_math', - 'text': m.group(1) or m.group(2) - }) - - def parse_latex_environment(self, m): - """Add token for a \begin{.} ...LaTeX stuff... \end{.} block""" - self.tokens.append({ - 'type': 'latex_environment', - 'name': m.group(1), - 'text': m.group(2) + "type": "multiline_math", + "text": m.group(1) or m.group(2) or m.group(3) }) @@ -88,12 +81,9 @@ def __init__(self, renderer, **kwargs): kwargs['block'] = MathBlockLexer super(MarkdownWithMath, self).__init__(renderer, **kwargs) - def output_block_math(self): - return self.renderer.block_math(self.token["text"]) - - def output_latex_environment(self): - return self.renderer.latex_environment(self.token["name"], - self.token["text"]) + + def output_multiline_math(self): + return self.inline(self.token["text"]) class IPythonRenderer(mistune.Renderer): From 2ab8b3f9185a992ca69f2d606307bf2be960ccdb Mon Sep 17 00:00:00 2001 From: M Pacer Date: Wed, 6 Dec 2017 10:02:30 -0800 Subject: [PATCH 062/671] add docstrings to explain the logic of the classes --- nbconvert/filters/markdown_mistune.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/nbconvert/filters/markdown_mistune.py b/nbconvert/filters/markdown_mistune.py index 1d418ac4b..6c85fcc69 100644 --- a/nbconvert/filters/markdown_mistune.py +++ b/nbconvert/filters/markdown_mistune.py @@ -22,6 +22,10 @@ class MathBlockGrammar(mistune.BlockGrammar): + """This defines a single regex comprised of the different patterns that + identify math content spanning multiple lines. These are used by the + MathBlockLexer. + """ multi_math_str = "|".join([r"(^\$\$.*?\$\$)", r"(^\\\\\[.*?\\\\\])", r"(^\\begin\{([a-z]*\*?)\}(.*?)\\end\{\4\})"]) @@ -29,6 +33,9 @@ class MathBlockGrammar(mistune.BlockGrammar): class MathInlineGrammar(mistune.InlineGrammar): + """This defines different ways of declaring math objects that should be + passed through to mathjax unaffected. These are used by the MathInlineLexer. + """ inline_math = re.compile(r"^\$(.+?)\$|^\\\\\((.+?)\\\\\)", re.DOTALL) block_math = re.compile(r"^\$\$(.*?)\$\$|^\\\\\[(.*?)\\\\\]", re.DOTALL) latex_environment = re.compile(r"^\\begin\{([a-z]*\*?)\}(.*?)\\end\{\1\}", @@ -37,6 +44,10 @@ class MathInlineGrammar(mistune.InlineGrammar): class MathBlockLexer(mistune.BlockLexer): + """ This acts as a pass-through to the MathInlineLexer. It is needed in + order to avoid other block level rules splitting math sections apart. + """ + default_rules = (['multiline_math'] + mistune.BlockLexer.default_rules) @@ -54,6 +65,14 @@ def parse_multiline_math(self, m): class MathInlineLexer(mistune.InlineLexer): + """This interprets the content of LaTeX style math objects using the rules + defined by the MathInlineGrammar. + + In particular this grabs ``$$...$$``, ``\\[...\\]``, ``\\(...\\)``, ``$...$``, + and ``\begin{foo}...\end{foo}`` styles for declaring mathematics. It strips + delimiters from all these varieties, and extracts the type of environment + in the last case (``foo`` in this example). + """ default_rules = (['block_math', 'inline_math', 'latex_environment'] + mistune.InlineLexer.default_rules) From d0105e7e29959936b229b0059dd9bc7e02e2aff3 Mon Sep 17 00:00:00 2001 From: M Pacer Date: Wed, 6 Dec 2017 10:56:13 -0800 Subject: [PATCH 063/671] Use m.group(0) as it captures the entirety of the main captured group thx @takluyver! --- nbconvert/filters/markdown_mistune.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nbconvert/filters/markdown_mistune.py b/nbconvert/filters/markdown_mistune.py index 6c85fcc69..17d3672d7 100644 --- a/nbconvert/filters/markdown_mistune.py +++ b/nbconvert/filters/markdown_mistune.py @@ -60,7 +60,7 @@ def parse_multiline_math(self, m): """Add token to pass through mutiline math.""" self.tokens.append({ "type": "multiline_math", - "text": m.group(1) or m.group(2) or m.group(3) + "text": m.group(0) }) From 6a2fd6ba88e792e2c3fb5572cfb4f7062707ea1e Mon Sep 17 00:00:00 2001 From: M Pacer Date: Wed, 6 Dec 2017 16:21:57 -0800 Subject: [PATCH 064/671] replace m.group(0) with m.string, arrange associated Grammars and Lexers --- nbconvert/filters/markdown_mistune.py | 30 +++++++++++++-------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/nbconvert/filters/markdown_mistune.py b/nbconvert/filters/markdown_mistune.py index 17d3672d7..abe70883d 100644 --- a/nbconvert/filters/markdown_mistune.py +++ b/nbconvert/filters/markdown_mistune.py @@ -26,23 +26,12 @@ class MathBlockGrammar(mistune.BlockGrammar): identify math content spanning multiple lines. These are used by the MathBlockLexer. """ - multi_math_str = "|".join([r"(^\$\$.*?\$\$)", - r"(^\\\\\[.*?\\\\\])", - r"(^\\begin\{([a-z]*\*?)\}(.*?)\\end\{\4\})"]) + multi_math_str = "|".join([r"^\$\$.*?\$\$", + r"^\\\\\[.*?\\\\\]", + r"^\\begin\{([a-z]*\*?)\}(.*?)\\end\{\1\}"]) multiline_math = re.compile(multi_math_str, re.DOTALL) -class MathInlineGrammar(mistune.InlineGrammar): - """This defines different ways of declaring math objects that should be - passed through to mathjax unaffected. These are used by the MathInlineLexer. - """ - inline_math = re.compile(r"^\$(.+?)\$|^\\\\\((.+?)\\\\\)", re.DOTALL) - block_math = re.compile(r"^\$\$(.*?)\$\$|^\\\\\[(.*?)\\\\\]", re.DOTALL) - latex_environment = re.compile(r"^\\begin\{([a-z]*\*?)\}(.*?)\\end\{\1\}", - re.DOTALL) - text = re.compile(r'^[\s\S]+?(?=[\\ Date: Fri, 8 Dec 2017 13:01:52 -0800 Subject: [PATCH 065/671] return m.string to m.group(0); simplify and escape new tests --- nbconvert/filters/markdown_mistune.py | 2 +- nbconvert/filters/tests/test_markdown.py | 24 ++++++++++++------------ 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/nbconvert/filters/markdown_mistune.py b/nbconvert/filters/markdown_mistune.py index abe70883d..280ae86ef 100644 --- a/nbconvert/filters/markdown_mistune.py +++ b/nbconvert/filters/markdown_mistune.py @@ -49,7 +49,7 @@ def parse_multiline_math(self, m): """Add token to pass through mutiline math.""" self.tokens.append({ "type": "multiline_math", - "text": m.string + "text": m.group(0) }) diff --git a/nbconvert/filters/tests/test_markdown.py b/nbconvert/filters/tests/test_markdown.py index 76fd8ca7a..d25f524e8 100644 --- a/nbconvert/filters/tests/test_markdown.py +++ b/nbconvert/filters/tests/test_markdown.py @@ -137,18 +137,18 @@ def test_markdown2html_math(self): "$$aa;a-b<0$$", "$$$$", - """$$x -= -2$$""", - r"""$$ - b_{l_1,l_2,l_3}^{\phi\phi\omega} = -8\, l_1\times l_2 \,l_1\cdot l_2 \int_0^{\chi_*} d\chi \frac{W(\chi,\chi_*)^2}{\chi^2} \int_0^{\chi}d\chi' \frac{W(\chi',\chi)W(\chi',\chi_*)}{{\chi'}^2} \left[ - P_\psi\left(\frac{l_1}{\chi},z(\chi)\right) P_\Psi\left(\frac{l_2}{\chi'},z(\chi')\right) -- (l_1\leftrightarrow l_2) -\right] -$$""", - r"""\begin{equation*} -x = 2 *55* 7 -\end{equation*}""", + ("$$x\n" + "=\n" + "2$$"), + ("$$\n" + "b = \\left[\n" + "P\\left(\\right)\n" + "- (l_1\\leftrightarrow l_2\n)" + "\\right]\n" + "$$"), + ("\\begin{equation*}\n" + "x = 2 *55* 7\n" + "\\end{equation*}"), """$ \\begin{tabular}{ l c r } 1 & 2 & 3 \\ From 62ce7d249b07446842b126ff799261b0cb3f5b42 Mon Sep 17 00:00:00 2001 From: Marco Rossi Date: Fri, 22 Dec 2017 15:30:06 +0100 Subject: [PATCH 066/671] Add set title to html and latex templates instead of if-else statement --- nbconvert/templates/html/full.tpl | 7 ++----- nbconvert/templates/html/slides_reveal.tpl | 7 ++----- nbconvert/templates/latex/base.tplx | 7 ++----- 3 files changed, 6 insertions(+), 15 deletions(-) diff --git a/nbconvert/templates/html/full.tpl b/nbconvert/templates/html/full.tpl index 133f427c5..66cb65231 100644 --- a/nbconvert/templates/html/full.tpl +++ b/nbconvert/templates/html/full.tpl @@ -1,5 +1,6 @@ {%- extends 'basic.tpl' -%} {% from 'mathjax.tpl' import mathjax %} +{% set nb_title = nb.metadata.get('title', '') or resources['metadata']['name'] %} {%- block header -%} @@ -8,11 +9,7 @@ {%- block html_head -%} -{%- if nb.metadata.get('title','') -%} -{{nb.metadata.get('title', '')}} -{%- else -%} -{{resources['metadata']['name']}} -{%- endif -%} +{{nb_title}} {%- if "widgets" in nb.metadata -%} diff --git a/nbconvert/templates/html/slides_reveal.tpl b/nbconvert/templates/html/slides_reveal.tpl index 4fb54eb81..2733e4e31 100644 --- a/nbconvert/templates/html/slides_reveal.tpl +++ b/nbconvert/templates/html/slides_reveal.tpl @@ -1,5 +1,6 @@ {%- extends 'basic.tpl' -%} {% from 'mathjax.tpl' import mathjax %} +{% set nb_title = nb.metadata.get('title', '') or resources['metadata']['name'] %} {%- block any_cell scoped -%} {%- if cell.metadata.get('slide_start', False) -%} @@ -44,11 +45,7 @@ -{%- if nb.metadata.get('title','') -%} -{{nb.metadata.get('title', '')}} slides -{%- else -%} -{{resources['metadata']['name']}} slides -{%- endif -%} +{{nb_title}} slides diff --git a/nbconvert/templates/latex/base.tplx b/nbconvert/templates/latex/base.tplx index 222ea40f4..fd5e6b25a 100644 --- a/nbconvert/templates/latex/base.tplx +++ b/nbconvert/templates/latex/base.tplx @@ -4,6 +4,7 @@ functions. Figures, data_text, This template does not define a docclass, the inheriting class must define this.=)) ((*- extends 'document_contents.tplx' -*)) +((*- set nb_title = nb.metadata.get('title', '') or resources['metadata']['name'] -*)) %=============================================================================== % Abstract overrides @@ -145,11 +146,7 @@ This template does not define a docclass, the inheriting class must define this. % Document parameters % Document title ((* block title -*)) - ((*- if "title" in nb.metadata *)) - \title{((( nb.metadata.get("title", "") | ascii_only | escape_latex )))} - ((*- else *)) - \title{((( resources.metadata.name | ascii_only | escape_latex )))} - ((*- endif -*)) + \title{((( nb_title | ascii_only | escape_latex )))} ((*- endblock title *)) ((* block date *))((* endblock date *)) ((* block author *))((* endblock author *)) From 594ea46cb5d50cf8a95fadbfa3f3b81e11d2aa51 Mon Sep 17 00:00:00 2001 From: "Ya'aqov (James) Walker" Date: Tue, 26 Dec 2017 03:22:06 -0500 Subject: [PATCH 067/671] Added Ubuntu Linux Instructions I've linked to a TeX Stackexchange article which has a straightforward answer on installing Xelatex on Ubuntu Linux (essential for saving Jupyter notebooks as PDFs). --- docs/source/install.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/source/install.rst b/docs/source/install.rst index 1bd6febbf..d4dfc1536 100644 --- a/docs/source/install.rst +++ b/docs/source/install.rst @@ -57,6 +57,7 @@ Fortunately, there are packages that make this much easier. These packages are specific to different operating systems: * Linux: `TeX Live `_ + * For Installing Xelatex on Ubuntu: https://tex.stackexchange.com/questions/179778/xelatex-under-ubuntu * macOS (OS X): `MacTeX `_. * Windows: `MikTex `_ From 96e86a72e78f2e5548c08a593fe02968e94b09dc Mon Sep 17 00:00:00 2001 From: Marco Rossi Date: Fri, 29 Dec 2017 22:35:55 +0100 Subject: [PATCH 068/671] Move command for setting title --- nbconvert/templates/html/full.tpl | 2 +- nbconvert/templates/html/slides_reveal.tpl | 2 +- nbconvert/templates/latex/base.tplx | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/nbconvert/templates/html/full.tpl b/nbconvert/templates/html/full.tpl index 66cb65231..1443e1160 100644 --- a/nbconvert/templates/html/full.tpl +++ b/nbconvert/templates/html/full.tpl @@ -1,6 +1,5 @@ {%- extends 'basic.tpl' -%} {% from 'mathjax.tpl' import mathjax %} -{% set nb_title = nb.metadata.get('title', '') or resources['metadata']['name'] %} {%- block header -%} @@ -9,6 +8,7 @@ {%- block html_head -%} +{% set nb_title = nb.metadata.get('title', '') or resources['metadata']['name'] %} {{nb_title}} {%- if "widgets" in nb.metadata -%} diff --git a/nbconvert/templates/html/slides_reveal.tpl b/nbconvert/templates/html/slides_reveal.tpl index 2733e4e31..0ddf84473 100644 --- a/nbconvert/templates/html/slides_reveal.tpl +++ b/nbconvert/templates/html/slides_reveal.tpl @@ -1,6 +1,5 @@ {%- extends 'basic.tpl' -%} {% from 'mathjax.tpl' import mathjax %} -{% set nb_title = nb.metadata.get('title', '') or resources['metadata']['name'] %} {%- block any_cell scoped -%} {%- if cell.metadata.get('slide_start', False) -%} @@ -45,6 +44,7 @@ +{% set nb_title = nb.metadata.get('title', '') or resources['metadata']['name'] %} {{nb_title}} slides diff --git a/nbconvert/templates/latex/base.tplx b/nbconvert/templates/latex/base.tplx index fd5e6b25a..0582eb657 100644 --- a/nbconvert/templates/latex/base.tplx +++ b/nbconvert/templates/latex/base.tplx @@ -4,7 +4,6 @@ functions. Figures, data_text, This template does not define a docclass, the inheriting class must define this.=)) ((*- extends 'document_contents.tplx' -*)) -((*- set nb_title = nb.metadata.get('title', '') or resources['metadata']['name'] -*)) %=============================================================================== % Abstract overrides @@ -146,6 +145,7 @@ This template does not define a docclass, the inheriting class must define this. % Document parameters % Document title ((* block title -*)) + ((*- set nb_title = nb.metadata.get('title', '') or resources['metadata']['name'] -*)) \title{((( nb_title | ascii_only | escape_latex )))} ((*- endblock title *)) ((* block date *))((* endblock date *)) From 4f23786b2048fabfaacdb51006955ace3787332b Mon Sep 17 00:00:00 2001 From: pacahon Date: Tue, 9 Jan 2018 15:25:15 +0300 Subject: [PATCH 069/671] fix for an issue with empty math block --- nbconvert/exporters/tests/files/notebook2.ipynb | 7 +++++++ nbconvert/filters/markdown_mistune.py | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/nbconvert/exporters/tests/files/notebook2.ipynb b/nbconvert/exporters/tests/files/notebook2.ipynb index 8d0caf3e3..34d95e6ae 100644 --- a/nbconvert/exporters/tests/files/notebook2.ipynb +++ b/nbconvert/exporters/tests/files/notebook2.ipynb @@ -172,6 +172,13 @@ "\n", "one *test* two *tests*. three *tests*" ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Make sure markdown parser doesn't crash with empty Latex formulas blocks\n$$$$\n$$" + ] } ], "metadata": { diff --git a/nbconvert/filters/markdown_mistune.py b/nbconvert/filters/markdown_mistune.py index 280ae86ef..8435e0ed9 100644 --- a/nbconvert/filters/markdown_mistune.py +++ b/nbconvert/filters/markdown_mistune.py @@ -85,7 +85,7 @@ def output_inline_math(self, m): return self.renderer.inline_math(m.group(1) or m.group(2)) def output_block_math(self, m): - return self.renderer.block_math(m.group(1) or m.group(2)) + return self.renderer.block_math(m.group(1) or m.group(2) or "") def output_latex_environment(self, m): return self.renderer.latex_environment(m.group(1), From d0efd4e16d5f48a2cc8a8b6301bdc6c67e0e1165 Mon Sep 17 00:00:00 2001 From: pacahon Date: Tue, 9 Jan 2018 22:16:34 +0300 Subject: [PATCH 070/671] fixed test for empty math block --- nbconvert/exporters/tests/files/notebook2.ipynb | 7 ------- nbconvert/filters/markdown_mistune.py | 2 +- nbconvert/filters/tests/test_markdown.py | 6 +++++- 3 files changed, 6 insertions(+), 9 deletions(-) diff --git a/nbconvert/exporters/tests/files/notebook2.ipynb b/nbconvert/exporters/tests/files/notebook2.ipynb index 34d95e6ae..8d0caf3e3 100644 --- a/nbconvert/exporters/tests/files/notebook2.ipynb +++ b/nbconvert/exporters/tests/files/notebook2.ipynb @@ -172,13 +172,6 @@ "\n", "one *test* two *tests*. three *tests*" ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Make sure markdown parser doesn't crash with empty Latex formulas blocks\n$$$$\n$$" - ] } ], "metadata": { diff --git a/nbconvert/filters/markdown_mistune.py b/nbconvert/filters/markdown_mistune.py index 8435e0ed9..1515fddb4 100644 --- a/nbconvert/filters/markdown_mistune.py +++ b/nbconvert/filters/markdown_mistune.py @@ -134,7 +134,7 @@ def escape_html(self, text): return cgi.escape(text) def block_math(self, text): - return '$$%s$$' % self.escape_html(text) + return '$$%s$$' % self.escape_html(text) if text else '' def latex_environment(self, name, text): name = self.escape_html(name) diff --git a/nbconvert/filters/tests/test_markdown.py b/nbconvert/filters/tests/test_markdown.py index d25f524e8..53d56e3ca 100644 --- a/nbconvert/filters/tests/test_markdown.py +++ b/nbconvert/filters/tests/test_markdown.py @@ -13,7 +13,7 @@ from ...tests.base import TestsBase from ..pandoc import convert_pandoc -from ..markdown import markdown2html +from ..markdown import markdown2html, markdown2html_mistune from jinja2 import Environment @@ -103,6 +103,10 @@ def test_pandoc_extra_args(self): self.assertEqual(latex.strip(), 'latex %s' % long_line) self.assertEqual(rst.strip(), 'rst %s' % long_line.replace(' ', '\n')) + def test_markdown2html_mistune_empty_math_block(self): + rendered = markdown2html_mistune("$$$$") + assert "$$$$" not in rendered + def test_markdown2html(self): """markdown2html test""" for index, test in enumerate(self.tests): From 378a89a60dc825c4e58c50038e46ef81b61d76f0 Mon Sep 17 00:00:00 2001 From: pacahon Date: Wed, 10 Jan 2018 12:23:16 +0300 Subject: [PATCH 071/671] fixed tests for empty math block --- nbconvert/exporters/tests/files/notebook2.ipynb | 10 ++++++++++ nbconvert/filters/markdown_mistune.py | 2 +- nbconvert/filters/tests/test_markdown.py | 8 ++++---- 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/nbconvert/exporters/tests/files/notebook2.ipynb b/nbconvert/exporters/tests/files/notebook2.ipynb index 8d0caf3e3..b1f0a553d 100644 --- a/nbconvert/exporters/tests/files/notebook2.ipynb +++ b/nbconvert/exporters/tests/files/notebook2.ipynb @@ -172,6 +172,16 @@ "\n", "one *test* two *tests*. three *tests*" ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Make sure markdown parser doesn't crash with empty Latex formulas blocks\n", + "$$$$\n", + "\\[\\]\n", + "$$" + ] } ], "metadata": { diff --git a/nbconvert/filters/markdown_mistune.py b/nbconvert/filters/markdown_mistune.py index 1515fddb4..8435e0ed9 100644 --- a/nbconvert/filters/markdown_mistune.py +++ b/nbconvert/filters/markdown_mistune.py @@ -134,7 +134,7 @@ def escape_html(self, text): return cgi.escape(text) def block_math(self, text): - return '$$%s$$' % self.escape_html(text) if text else '' + return '$$%s$$' % self.escape_html(text) def latex_environment(self, name, text): name = self.escape_html(name) diff --git a/nbconvert/filters/tests/test_markdown.py b/nbconvert/filters/tests/test_markdown.py index 53d56e3ca..96717b58f 100644 --- a/nbconvert/filters/tests/test_markdown.py +++ b/nbconvert/filters/tests/test_markdown.py @@ -32,6 +32,8 @@ class TestMarkdown(TestsBase): '##test', 'test\n----', 'test [link](https://google.com/)', + '$$$$', + '\\\\[\\\\]', ] tokens = [ @@ -46,6 +48,8 @@ class TestMarkdown(TestsBase): 'test', 'test', ('test', 'https://google.com/'), + '$$$$', + '$$$$', ] @dec.onlyif_cmds_exist('pandoc') @@ -103,10 +107,6 @@ def test_pandoc_extra_args(self): self.assertEqual(latex.strip(), 'latex %s' % long_line) self.assertEqual(rst.strip(), 'rst %s' % long_line.replace(' ', '\n')) - def test_markdown2html_mistune_empty_math_block(self): - rendered = markdown2html_mistune("$$$$") - assert "$$$$" not in rendered - def test_markdown2html(self): """markdown2html test""" for index, test in enumerate(self.tests): From 64a776ded8ac87158ba3a067149dbdebbb8943ee Mon Sep 17 00:00:00 2001 From: pacahon Date: Wed, 10 Jan 2018 12:26:36 +0300 Subject: [PATCH 072/671] remove unused imports --- nbconvert/filters/tests/test_markdown.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nbconvert/filters/tests/test_markdown.py b/nbconvert/filters/tests/test_markdown.py index 96717b58f..189e71531 100644 --- a/nbconvert/filters/tests/test_markdown.py +++ b/nbconvert/filters/tests/test_markdown.py @@ -13,7 +13,7 @@ from ...tests.base import TestsBase from ..pandoc import convert_pandoc -from ..markdown import markdown2html, markdown2html_mistune +from ..markdown import markdown2html from jinja2 import Environment From 14fb4d60f3e00dc16e0c689e296460e923da041f Mon Sep 17 00:00:00 2001 From: pacahon Date: Wed, 10 Jan 2018 13:00:45 +0300 Subject: [PATCH 073/671] fix empty math block test again --- nbconvert/filters/tests/test_markdown.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/nbconvert/filters/tests/test_markdown.py b/nbconvert/filters/tests/test_markdown.py index 189e71531..6e3518976 100644 --- a/nbconvert/filters/tests/test_markdown.py +++ b/nbconvert/filters/tests/test_markdown.py @@ -32,8 +32,6 @@ class TestMarkdown(TestsBase): '##test', 'test\n----', 'test [link](https://google.com/)', - '$$$$', - '\\\\[\\\\]', ] tokens = [ @@ -48,8 +46,6 @@ class TestMarkdown(TestsBase): 'test', 'test', ('test', 'https://google.com/'), - '$$$$', - '$$$$', ] @dec.onlyif_cmds_exist('pandoc') @@ -189,7 +185,10 @@ def test_markdown2html_math_mixed(self): C_{ik} = \sum_{j=1}^n A_{ij} B_{jk}, $$ but you can _implement_ this computation in many ways. -$\approx 2mnp$ flops are needed for \\\\[ C_{ik} = \sum_{j=1}^n A_{ij} B_{jk} \\\\].""" +$\approx 2mnp$ flops are needed for \\\\[ C_{ik} = \sum_{j=1}^n A_{ij} B_{jk} \\\\]. +Also check empty math blocks work correctly: +$$$$ +\\\\[\\\\]""" output_check = (case.replace("_implement_", "implement") .replace("\\\\(", "$").replace("\\\\)", "$") .replace("\\\\[", "$$").replace("\\\\]", "$$")) From 82d6979fb9c1a593b661d8bff362b099bcd80d8d Mon Sep 17 00:00:00 2001 From: M Pacer Date: Sat, 13 Jan 2018 15:25:25 -0800 Subject: [PATCH 074/671] better prompt hiding logic (don't remove all code from latex) --- nbconvert/templates/html/basic.tpl | 42 +++++++++---------- .../templates/latex/style_bw_ipython.tplx | 8 ++++ nbconvert/templates/latex/style_ipython.tplx | 8 ++++ 3 files changed, 37 insertions(+), 21 deletions(-) diff --git a/nbconvert/templates/html/basic.tpl b/nbconvert/templates/html/basic.tpl index eaec1f701..380f889b1 100644 --- a/nbconvert/templates/html/basic.tpl +++ b/nbconvert/templates/html/basic.tpl @@ -23,12 +23,12 @@ {% block in_prompt -%}
-{%- if cell.execution_count is defined -%} {%- if resources.global_content_filter.include_input_prompt-%} -In [{{ cell.execution_count|replace(None, " ") }}]: -{%- else -%} -In [ ]: -{%- endif -%} + {%- if cell.execution_count is defined -%} + In [{{ cell.execution_count|replace(None, " ") }}]: + {%- else -%} + In [ ]: + {%- endif -%} {%- endif -%}
{%- endblock in_prompt %} @@ -51,26 +51,26 @@ In [ ]:
{{ cell.source | highlight_code(metadata=cell.metadata) }} -
+
{%- endblock input %} {% block output %}
{% if resources.global_content_filter.include_output_prompt %} -{% block output_area_prompt %} -{%- if output.output_type == 'execute_result' -%} -
-{%- if cell.execution_count is defined -%} - Out[{{ cell.execution_count|replace(None, " ") }}]: -{%- else -%} - Out[ ]: -{%- endif -%} -{%- else -%} -
-{%- endif -%} -
-{% endblock output_area_prompt %} + {% block output_area_prompt %} + {%- if output.output_type == 'execute_result' -%} +
+ {%- if cell.execution_count is defined -%} + Out[{{ cell.execution_count|replace(None, " ") }}]: + {%- else -%} + Out[ ]: + {%- endif -%} + {%- else -%} +
+ {%- endif -%} +
+ {% endblock output_area_prompt %} {% endif %} {{ super() }}
@@ -79,7 +79,7 @@ In [ ]: {% block markdowncell scoped %}
{%- if resources.global_content_filter.include_input_prompt-%} -{{ self.empty_in_prompt() }} + {{ self.empty_in_prompt() }} {%- endif -%}
@@ -97,7 +97,7 @@ unknown type {{ cell.type }} {%- set extra_class="output_execute_result" -%} {% block data_priority scoped %} {{ super() }} -{% endblock %} +{% endblock data_priority %} {%- set extra_class="" -%} {%- endblock execute_result %} diff --git a/nbconvert/templates/latex/style_bw_ipython.tplx b/nbconvert/templates/latex/style_bw_ipython.tplx index 9309f9196..43ccc5563 100644 --- a/nbconvert/templates/latex/style_bw_ipython.tplx +++ b/nbconvert/templates/latex/style_bw_ipython.tplx @@ -9,6 +9,8 @@ ((* block input scoped *)) ((*- if resources.global_content_filter.include_input_prompt *)) ((( add_prompt(cell.source, cell, 'In ') ))) +((* else *)) +(((cell.source))) ((* endif -*)) ((* endblock input *)) @@ -25,6 +27,12 @@ ((*- else -*)) \verb+Out[((( cell.execution_count )))]:+((( super() ))) ((*- endif -*)) + ((*- else -*)) + ((*- if type in ['text/plain'] *)) +((( output.data['text/plain'] ))) + ((*- else -*)) +\verb+((( super() ))) + ((*- endif -*)) ((*- endif -*)) ((*- endfor -*)) ((* endblock execute_result *)) diff --git a/nbconvert/templates/latex/style_ipython.tplx b/nbconvert/templates/latex/style_ipython.tplx index 7a3ab8c3b..10f01a80b 100644 --- a/nbconvert/templates/latex/style_ipython.tplx +++ b/nbconvert/templates/latex/style_ipython.tplx @@ -22,6 +22,8 @@ ((* block input scoped *)) ((*- if resources.global_content_filter.include_input_prompt *)) ((( add_prompt(cell.source | highlight_code(strip_verbatim=True, metadata=cell.metadata), cell, 'In ', 'incolor') ))) +((*- else *)) + ((( cell.source | highlight_code(strip_verbatim=True, metadata=cell.metadata) ))) ((* endif *)) ((* endblock input *)) @@ -38,6 +40,12 @@ ((* else -*)) \texttt{\color{outcolor}Out[{\color{outcolor}((( cell.execution_count )))}]:}((( super() ))) ((*- endif -*)) + ((*- else -*)) + ((*- if type in ['text/plain'] *)) +((( output.data['text/plain'] | escape_latex ))) + ((* else -*)) +((( super() ))) + ((*- endif -*)) ((*- endif -*)) ((*- endfor -*)) ((* endblock execute_result *)) From 4ff26f8df5d9a237fb28a9cdcc58614b7f9082dd Mon Sep 17 00:00:00 2001 From: M Pacer Date: Sat, 13 Jan 2018 15:57:19 -0800 Subject: [PATCH 075/671] add test to latex to make sure input content remains after removing prompts --- nbconvert/exporters/tests/test_latex.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/nbconvert/exporters/tests/test_latex.py b/nbconvert/exporters/tests/test_latex.py index 17a33851c..926c753b8 100644 --- a/nbconvert/exporters/tests/test_latex.py +++ b/nbconvert/exporters/tests/test_latex.py @@ -9,6 +9,8 @@ from .base import ExportersTestsBase from ..latex import LatexExporter + +from traitlets.config import Config from nbformat import write from nbformat import v4 from ipython_genutils.testing.decorators import onlyif_cmds_exist @@ -117,6 +119,26 @@ def test_prompt_number_color(self): assert re.findall(in_regex, output) == ins assert re.findall(out_regex, output) == outs + @onlyif_cmds_exist('pandoc') + def test_no_prompt_yes_input(self): + no_prompt = { + "TemplateExporter":{ + "exclude_output": False, + "exclude_input": False, + "exclude_input_prompt": True, + "exclude_output_prompt": True, + "exclude_markdown": False, + "exclude_code_cell": False, + } + } + c_no_prompt = Config(no_prompt) + + exporter = LatexExporter(config=c_no_prompt) + (output, resources) = exporter.from_filename( + self._get_notebook(nb_name="prompt_numbers.ipynb")) + assert "shape" in output + assert "evs" in output + def test_in_memory_template_tplx(self): # Loads in an in memory latex template (.tplx) using jinja2.DictLoader # creates a class that uses this template with the template_file argument From 8c71fa6c8249b8f11d54eee6f144d31adb41b6e1 Mon Sep 17 00:00:00 2001 From: M Pacer Date: Sat, 13 Jan 2018 16:14:37 -0800 Subject: [PATCH 076/671] add test to make sure that html properly removes prompts --- nbconvert/exporters/tests/test_html.py | 28 +++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/nbconvert/exporters/tests/test_html.py b/nbconvert/exporters/tests/test_html.py index c7d5b20a5..1d6c3a97c 100644 --- a/nbconvert/exporters/tests/test_html.py +++ b/nbconvert/exporters/tests/test_html.py @@ -3,10 +3,13 @@ # Copyright (c) IPython Development Team. # Distributed under the terms of the Modified BSD License. +import re + from .base import ExportersTestsBase from ..html import HTMLExporter + +from traitlets.config import Config from nbformat import v4 -import re class TestHTMLExporter(ExportersTestsBase): @@ -59,6 +62,29 @@ def test_prompt_number(self): assert re.findall(in_regex, output) == ins assert re.findall(out_regex, output) == outs + + def test_prompt_number(self): + """ + Does HTMLExporter properly format input and output prompts? + """ + no_prompt_conf = Config( + {"TemplateExporter":{ + "exclude_input_prompt": True, + "exclude_output_prompt": True, + } + } + ) + exporter = HTMLExporter(config=no_prompt_conf, template_file='full') + (output, resources) = exporter.from_filename( + self._get_notebook(nb_name="prompt_numbers.ipynb")) + in_regex = r"In \[(.*)\]:" + out_regex = r"Out\[(.*)\]:" + + ins = ["2", "10", " ", " ", "0"] + outs = ["10"] + + assert not re.findall(in_regex, output) + assert not re.findall(out_regex, output) def test_png_metadata(self): """ From b4fc3b567acabf3716b41f071039d2f1a45b0619 Mon Sep 17 00:00:00 2001 From: M Pacer Date: Sat, 13 Jan 2018 22:13:58 -0800 Subject: [PATCH 077/671] add tests for asciidoc; improve tests for html/latex; html template --- nbconvert/exporters/tests/test_asciidoc.py | 28 ++++++++++++++++++- nbconvert/exporters/tests/test_html.py | 3 -- nbconvert/exporters/tests/test_latex.py | 4 --- nbconvert/templates/html/basic.tpl | 32 ++++++++++------------ 4 files changed, 42 insertions(+), 25 deletions(-) diff --git a/nbconvert/exporters/tests/test_asciidoc.py b/nbconvert/exporters/tests/test_asciidoc.py index be08077b4..65075f9e9 100644 --- a/nbconvert/exporters/tests/test_asciidoc.py +++ b/nbconvert/exporters/tests/test_asciidoc.py @@ -12,9 +12,13 @@ # Imports #----------------------------------------------------------------------------- +import re + +from traitlets.config import Config +from ipython_genutils.testing import decorators as dec + from .base import ExportersTestsBase from ..asciidoc import ASCIIDocExporter -from ipython_genutils.testing import decorators as dec #----------------------------------------------------------------------------- # Class @@ -39,3 +43,25 @@ def test_export(self): """ (output, resources) = ASCIIDocExporter().from_filename(self._get_notebook()) assert len(output) > 0 + + @dec.onlyif_cmds_exist('pandoc') + def test_export(self): + """ + Can a ASCIIDocExporter export something? + """ + no_prompt = { + "TemplateExporter":{ + "exclude_input_prompt": True, + "exclude_output_prompt": True, + } + } + c_no_prompt = Config(no_prompt) + exporter = ASCIIDocExporter(config=c_no_prompt) + (output, resources) = exporter.from_filename( + self._get_notebook(nb_name="prompt_numbers.ipynb")) + + in_regex = r"In \[(.*)\]:" + out_regex = r"Out\[(.*)\]:" + + assert not re.findall(in_regex, output) + assert not re.findall(out_regex, output) diff --git a/nbconvert/exporters/tests/test_html.py b/nbconvert/exporters/tests/test_html.py index 1d6c3a97c..1a771e023 100644 --- a/nbconvert/exporters/tests/test_html.py +++ b/nbconvert/exporters/tests/test_html.py @@ -80,9 +80,6 @@ def test_prompt_number(self): in_regex = r"In \[(.*)\]:" out_regex = r"Out\[(.*)\]:" - ins = ["2", "10", " ", " ", "0"] - outs = ["10"] - assert not re.findall(in_regex, output) assert not re.findall(out_regex, output) diff --git a/nbconvert/exporters/tests/test_latex.py b/nbconvert/exporters/tests/test_latex.py index 926c753b8..b58273293 100644 --- a/nbconvert/exporters/tests/test_latex.py +++ b/nbconvert/exporters/tests/test_latex.py @@ -123,12 +123,8 @@ def test_prompt_number_color(self): def test_no_prompt_yes_input(self): no_prompt = { "TemplateExporter":{ - "exclude_output": False, - "exclude_input": False, "exclude_input_prompt": True, "exclude_output_prompt": True, - "exclude_markdown": False, - "exclude_code_cell": False, } } c_no_prompt = Config(no_prompt) diff --git a/nbconvert/templates/html/basic.tpl b/nbconvert/templates/html/basic.tpl index 380f889b1..9159415ab 100644 --- a/nbconvert/templates/html/basic.tpl +++ b/nbconvert/templates/html/basic.tpl @@ -23,21 +23,17 @@ {% block in_prompt -%}
-{%- if resources.global_content_filter.include_input_prompt-%} {%- if cell.execution_count is defined -%} In [{{ cell.execution_count|replace(None, " ") }}]: {%- else -%} In [ ]: {%- endif -%} -{%- endif -%}
{%- endblock in_prompt %} {% block empty_in_prompt -%} -{%- if resources.global_content_filter.include_input_prompt-%}
-{% endif %} {%- endblock empty_in_prompt %} {# @@ -55,22 +51,24 @@
{%- endblock input %} +{% block output_area_prompt %} +{%- if output.output_type == 'execute_result' -%} +
+ {%- if cell.execution_count is defined -%} + Out[{{ cell.execution_count|replace(None, " ") }}]: + {%- else -%} + Out[ ]: + {%- endif -%} +{%- else -%} +
+{%- endif -%} +
+{% endblock output_area_prompt %} + {% block output %}
{% if resources.global_content_filter.include_output_prompt %} - {% block output_area_prompt %} - {%- if output.output_type == 'execute_result' -%} -
- {%- if cell.execution_count is defined -%} - Out[{{ cell.execution_count|replace(None, " ") }}]: - {%- else -%} - Out[ ]: - {%- endif -%} - {%- else -%} -
- {%- endif -%} -
- {% endblock output_area_prompt %} + {{ self.output_area_prompt() }} {% endif %} {{ super() }}
From 53d48fbe1af663314afaefb3f8f6e2bc139a0d0e Mon Sep 17 00:00:00 2001 From: Michael Droettboom Date: Wed, 17 Jan 2018 13:20:38 -0500 Subject: [PATCH 078/671] Fix Jinja syntax in custom template example. --- docs/source/external_exporters.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/external_exporters.rst b/docs/source/external_exporters.rst index d81108652..5aa96bda6 100644 --- a/docs/source/external_exporters.rst +++ b/docs/source/external_exporters.rst @@ -204,7 +204,7 @@ And the template file, that inherits from the html `full` template and prepend/a ## this is a markdown cell - {super()} + {{ super() }} ## THIS IS THE END From 8c5c3d20823d74a8b159e9334df07f30522beb27 Mon Sep 17 00:00:00 2001 From: Thomas Kluyver Date: Thu, 18 Jan 2018 10:51:59 +0000 Subject: [PATCH 079/671] Add failing test for gh-657 --- .../tests/files/Empty Cell.ipynb | 79 +++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 nbconvert/preprocessors/tests/files/Empty Cell.ipynb diff --git a/nbconvert/preprocessors/tests/files/Empty Cell.ipynb b/nbconvert/preprocessors/tests/files/Empty Cell.ipynb new file mode 100644 index 000000000..202dc194c --- /dev/null +++ b/nbconvert/preprocessors/tests/files/Empty Cell.ipynb @@ -0,0 +1,79 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Test that executing skips over an empty cell." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'Code 1'" + ] + }, + "execution_count": 1, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "\"Code 1\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'Code 2'" + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "\"Code 2\"" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.5.2" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} From f8f8cac8c1765a02106086d7d7df48c6a42140da Mon Sep 17 00:00:00 2001 From: Thomas Kluyver Date: Thu, 18 Jan 2018 10:55:00 +0000 Subject: [PATCH 080/671] Skip executing empty code cells This matches the behaviour of the notebook application. Closes gh-657 --- nbconvert/preprocessors/execute.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nbconvert/preprocessors/execute.py b/nbconvert/preprocessors/execute.py index f5cca4280..0dbc6d5c8 100644 --- a/nbconvert/preprocessors/execute.py +++ b/nbconvert/preprocessors/execute.py @@ -290,7 +290,7 @@ def preprocess_cell(self, cell, resources, cell_index): To execute all cells see :meth:`preprocess`. """ - if cell.cell_type != 'code': + if cell.cell_type != 'code' or not cell.source.strip(): return cell, resources reply, outputs = self.run_cell(cell, cell_index) From b2c7cb056591d23f87f515c5b3b761f935fd0fe6 Mon Sep 17 00:00:00 2001 From: M Pacer Date: Tue, 23 Jan 2018 10:01:33 -0800 Subject: [PATCH 081/671] test for prompts by default; update regex to be global for both tests --- nbconvert/exporters/tests/test_asciidoc.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/nbconvert/exporters/tests/test_asciidoc.py b/nbconvert/exporters/tests/test_asciidoc.py index 65075f9e9..9fb20be47 100644 --- a/nbconvert/exporters/tests/test_asciidoc.py +++ b/nbconvert/exporters/tests/test_asciidoc.py @@ -23,6 +23,8 @@ #----------------------------------------------------------------------------- # Class #----------------------------------------------------------------------------- +in_regex = r"In\[(.*)\]:" +out_regex = r"Out\[(.*)\]:" class TestASCIIDocExporter(ExportersTestsBase): """Tests for ASCIIDocExporter""" @@ -44,10 +46,13 @@ def test_export(self): (output, resources) = ASCIIDocExporter().from_filename(self._get_notebook()) assert len(output) > 0 + assert re.findall(in_regex, output) + assert re.findall(out_regex, output) + @dec.onlyif_cmds_exist('pandoc') - def test_export(self): + def test_export_no_prompt(self): """ - Can a ASCIIDocExporter export something? + Can a ASCIIDocExporter export something without prompts? """ no_prompt = { "TemplateExporter":{ @@ -60,8 +65,5 @@ def test_export(self): (output, resources) = exporter.from_filename( self._get_notebook(nb_name="prompt_numbers.ipynb")) - in_regex = r"In \[(.*)\]:" - out_regex = r"Out\[(.*)\]:" - assert not re.findall(in_regex, output) assert not re.findall(out_regex, output) From 2b74ad8abb4866105445af7ad427ea7c0fcaa836 Mon Sep 17 00:00:00 2001 From: M Pacer Date: Wed, 10 Jan 2018 12:54:27 -0800 Subject: [PATCH 082/671] change default for slides to direct to the cdn rather than locally, quick fix for #702 --- nbconvert/exporters/slides.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nbconvert/exporters/slides.py b/nbconvert/exporters/slides.py index 6f22b1776..8be0daada 100644 --- a/nbconvert/exporters/slides.py +++ b/nbconvert/exporters/slides.py @@ -90,7 +90,7 @@ def _reveal_url_prefix_default(self): warn("Please update RevealHelpPreprocessor.url_prefix to " "SlidesExporter.reveal_url_prefix in config files.") return self.config.RevealHelpPreprocessor.url_prefix - return 'reveal.js' + return 'https://cdnjs.cloudflare.com/ajax/libs/reveal.js/3.1.0' reveal_theme = Unicode('simple', help=""" From ff3ef1e4a0e5c3ecb21546d850af300f67d6454b Mon Sep 17 00:00:00 2001 From: Thomas Kluyver Date: Mon, 15 Jan 2018 15:49:03 +0000 Subject: [PATCH 083/671] Clarify that reveal.js 3.x is needed Closes gh-702 --- docs/source/usage.rst | 2 +- nbconvert/exporters/slides.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/source/usage.rst b/docs/source/usage.rst index 39e4a9b2d..cf95e7674 100644 --- a/docs/source/usage.rst +++ b/docs/source/usage.rst @@ -96,7 +96,7 @@ Reveal.js HTML slideshow ``--post serve`` on the command-line. The ``serve`` post-processor proxies Reveal.js requests to a CDN if no local Reveal.js library is present. To make slides that don't require an internet connection, just place the - Reveal.js library in the same directory where your_talk.slides.html is + Reveal.js library (version 3.x) in the same directory where ``your_talk.slides.html`` is located, or point to another directory using the ``--reveal-prefix`` alias. .. note:: diff --git a/nbconvert/exporters/slides.py b/nbconvert/exporters/slides.py index 8be0daada..e528b4913 100644 --- a/nbconvert/exporters/slides.py +++ b/nbconvert/exporters/slides.py @@ -77,7 +77,7 @@ class SlidesExporter(HTMLExporter): reveal_url_prefix = Unicode( help="""The URL prefix for reveal.js. - This can be a a relative URL for a local copy of reveal.js, + This can be a a relative URL for a local copy of reveal.js (version 3.x), or point to a CDN. For speaker notes to work, a local reveal.js prefix must be used. From 6fcd4a6191224bfb84007dead4c45ecdb14bb49b Mon Sep 17 00:00:00 2001 From: Thomas Kluyver Date: Thu, 18 Jan 2018 10:36:52 +0000 Subject: [PATCH 084/671] Instructions for fetching reveal.js 3.6.0 --- docs/source/usage.rst | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/docs/source/usage.rst b/docs/source/usage.rst index cf95e7674..3c6a0803d 100644 --- a/docs/source/usage.rst +++ b/docs/source/usage.rst @@ -99,6 +99,15 @@ Reveal.js HTML slideshow Reveal.js library (version 3.x) in the same directory where ``your_talk.slides.html`` is located, or point to another directory using the ``--reveal-prefix`` alias. + To get a compatible version of revealjs in the current folder: + + .. code-block:: shell + + git clone https://github.com/hakimel/reveal.js.git + cd reveal.js + git checkout 3.6.0 + cd .. + .. note:: In order to designate a mapping from notebook cells to Reveal.js slides, From b83a599f62bbded2e6d1a3773d30b042ef78b9c4 Mon Sep 17 00:00:00 2001 From: Thomas Kluyver Date: Fri, 19 Jan 2018 12:24:20 +0000 Subject: [PATCH 085/671] Use reveal 3.5 for consistency --- docs/source/usage.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/usage.rst b/docs/source/usage.rst index 3c6a0803d..6d65d6b2e 100644 --- a/docs/source/usage.rst +++ b/docs/source/usage.rst @@ -105,7 +105,7 @@ Reveal.js HTML slideshow git clone https://github.com/hakimel/reveal.js.git cd reveal.js - git checkout 3.6.0 + git checkout 3.5.0 cd .. .. note:: From 2f508ca9f13457fb5c074345196f45b7067e59a1 Mon Sep 17 00:00:00 2001 From: M Pacer Date: Tue, 23 Jan 2018 10:06:19 -0800 Subject: [PATCH 086/671] update version of reveal to 3.5 --- nbconvert/exporters/slides.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nbconvert/exporters/slides.py b/nbconvert/exporters/slides.py index e528b4913..8c1d212d7 100644 --- a/nbconvert/exporters/slides.py +++ b/nbconvert/exporters/slides.py @@ -90,7 +90,7 @@ def _reveal_url_prefix_default(self): warn("Please update RevealHelpPreprocessor.url_prefix to " "SlidesExporter.reveal_url_prefix in config files.") return self.config.RevealHelpPreprocessor.url_prefix - return 'https://cdnjs.cloudflare.com/ajax/libs/reveal.js/3.1.0' + return 'https://cdnjs.cloudflare.com/ajax/libs/reveal.js/3.5.0' reveal_theme = Unicode('simple', help=""" From 880a388d5ad3931ff07ad0b7b5b37555ea536b76 Mon Sep 17 00:00:00 2001 From: M Pacer Date: Thu, 25 Jan 2018 12:47:04 -0800 Subject: [PATCH 087/671] modify help string to point at the usage docs. --- nbconvert/exporters/slides.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/nbconvert/exporters/slides.py b/nbconvert/exporters/slides.py index 8c1d212d7..522534125 100644 --- a/nbconvert/exporters/slides.py +++ b/nbconvert/exporters/slides.py @@ -76,11 +76,16 @@ class SlidesExporter(HTMLExporter): """Exports HTML slides with reveal.js""" reveal_url_prefix = Unicode( - help="""The URL prefix for reveal.js. - This can be a a relative URL for a local copy of reveal.js (version 3.x), - or point to a CDN. - - For speaker notes to work, a local reveal.js prefix must be used. + help="""The URL prefix for reveal.js (version 3.x). + This defaults to the reveal CDN, but can be any url pointing to a copy + of reveal.js. + + For speaker notes to work, this must be a relative path to a local + copy of reveal.js: e.g., "reveal.js". + + See the usage documentation + (https://nbconvert.readthedocs.io/en/latest/usage.html#reveal-js-html-slideshow) + for more details. """ ).tag(config=True) From a6079579baf67b5d95daa6959bead65072ab3af4 Mon Sep 17 00:00:00 2001 From: M Pacer Date: Thu, 25 Jan 2018 14:54:38 -0800 Subject: [PATCH 088/671] Improve usage docs for the reveal slideshow emphasize that the key feature for speaker notes is a local copy. Give an example of how to set up speaker notes. --- docs/source/usage.rst | 85 ++++++++++++++++++++++++----------- nbconvert/exporters/slides.py | 3 +- 2 files changed, 62 insertions(+), 26 deletions(-) diff --git a/docs/source/usage.rst b/docs/source/usage.rst index 6d65d6b2e..2c6dd84bc 100644 --- a/docs/source/usage.rst +++ b/docs/source/usage.rst @@ -92,31 +92,66 @@ Reveal.js HTML slideshow * ``--to slides`` This generates a Reveal.js HTML slideshow. - It must be served by an HTTP server. The easiest way to do this is adding - ``--post serve`` on the command-line. The ``serve`` post-processor proxies - Reveal.js requests to a CDN if no local Reveal.js library is present. - To make slides that don't require an internet connection, just place the - Reveal.js library (version 3.x) in the same directory where ``your_talk.slides.html`` is - located, or point to another directory using the ``--reveal-prefix`` alias. - - To get a compatible version of revealjs in the current folder: - - .. code-block:: shell - - git clone https://github.com/hakimel/reveal.js.git - cd reveal.js - git checkout 3.5.0 - cd .. - - .. note:: - - In order to designate a mapping from notebook cells to Reveal.js slides, - from within the Jupyter notebook, select menu item - View --> Cell Toolbar --> Slideshow. That will reveal a drop-down menu - on the upper-right of each cell. From it, one may choose from - "Slide," "Sub-Slide", "Fragment", "Skip", and "Notes." On conversion, - cells designated as "skip" will not be included, "notes" will be included - only in presenter notes, etc. + +Running this slideshow requires a copy of reveal.js (version 3.x). + +By default, this will include a script tag in the html that will directly load +reveal.js from a CDN. + +However, some features (specifically, speaker notes) are only available if you +use a local copy of reveal.js. This requires that first you have a local copy +of reveal.js and then that you redirect the script away from your CDN to your +local copy. + +To make this clearer, let's look at an example. + +.. note:: + + In order to designate a mapping from notebook cells to Reveal.js slides, + from within the Jupyter notebook, select menu item + View --> Cell Toolbar --> Slideshow. That will reveal a drop-down menu + on the upper-right of each cell. From it, one may choose from + "Slide," "Sub-Slide", "Fragment", "Skip", and "Notes." On conversion, + cells designated as "skip" will not be included, "notes" will be included + only in presenter notes, etc. + +Example: creating slides w/ speaker notes +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Let's suppose you have a notebook ``your_talk.ipynb`` that you want to convert +to slides. For this example, we'll assume that you are working in the same +directory as the notebook you want to convert (i.e., when you run ``ls .``, +``your_talk.ipynb`` shows up amongst the list of files). + +First, we need a compatible version of reveal.js in the current folder run the +following commands inside the directory: + +.. code-block:: shell + + git clone https://github.com/hakimel/reveal.js.git + cd reveal.js + git checkout 3.5.0 + cd .. + +Then we need to tell nbconvert to point to this local copy. To do that we use +the ``--reveal-prefix`` command line flag to point to the local copy. + +.. code-block:: shell + + jupyter nbconvert your_talk.ipynb --to slides --reveal-prefix reveal.js + +This will create file ``your_talk.slides.html``, which you should be able to +access with ``open your_talk.slides.html``. To access the speaker notes, press +``s`` after the slides load and they should open in a new window. + +This should also allow you to use your slides without an internet connection. + +If this does not work, you can also try start a server as part of your nbconvert +command. To do this we use the ``ServePostProcessor``, which we activate by +appending the command line flag ``--post serve`` to the above command. This +will not allow you to use speaker notes if you do not have a local copy of +reveal.js. + .. _convert_markdown: diff --git a/nbconvert/exporters/slides.py b/nbconvert/exporters/slides.py index 522534125..fe1576267 100644 --- a/nbconvert/exporters/slides.py +++ b/nbconvert/exporters/slides.py @@ -101,7 +101,8 @@ def _reveal_url_prefix_default(self): help=""" Name of the reveal.js theme to use. - We look for a file with this name under `reveal_url_prefix`/css/theme/`reveal_theme`.css. + We look for a file with this name under + ``reveal_url_prefix``/css/theme/``reveal_theme``.css. https://github.com/hakimel/reveal.js/tree/master/css/theme has list of themes that ship by default with reveal.js. From b2254febd335cd4a9b08cb512ca0b4bd0b7bdbae Mon Sep 17 00:00:00 2001 From: Damian Avila Date: Fri, 26 Jan 2018 08:47:56 -0300 Subject: [PATCH 089/671] Remove offline statement and add some clarifications --- docs/source/usage.rst | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/docs/source/usage.rst b/docs/source/usage.rst index 2c6dd84bc..4913600c5 100644 --- a/docs/source/usage.rst +++ b/docs/source/usage.rst @@ -133,6 +133,10 @@ following commands inside the directory: git checkout 3.5.0 cd .. +Alternative, you can download a zip (or tar.gz) file containing reveal.js from +https://github.com/hakimel/reveal.js/releases/tag/3.5.0, but be sure to unzip +(or untar) the file to a directory named reveal.js. + Then we need to tell nbconvert to point to this local copy. To do that we use the ``--reveal-prefix`` command line flag to point to the local copy. @@ -142,9 +146,9 @@ the ``--reveal-prefix`` command line flag to point to the local copy. This will create file ``your_talk.slides.html``, which you should be able to access with ``open your_talk.slides.html``. To access the speaker notes, press -``s`` after the slides load and they should open in a new window. - -This should also allow you to use your slides without an internet connection. +``s`` after the slides load and they should open in a new window. Keep in mind +that if you want a functional timer inside the speaker notes, you need to serve +the slides (see next paragraph forß details). If this does not work, you can also try start a server as part of your nbconvert command. To do this we use the ``ServePostProcessor``, which we activate by From ab01365adad36960ece8636c1d5898e7d2f33642 Mon Sep 17 00:00:00 2001 From: Damian Avila Date: Fri, 26 Jan 2018 08:50:07 -0300 Subject: [PATCH 090/671] Remove a typo --- docs/source/usage.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/usage.rst b/docs/source/usage.rst index 4913600c5..616861451 100644 --- a/docs/source/usage.rst +++ b/docs/source/usage.rst @@ -148,7 +148,7 @@ This will create file ``your_talk.slides.html``, which you should be able to access with ``open your_talk.slides.html``. To access the speaker notes, press ``s`` after the slides load and they should open in a new window. Keep in mind that if you want a functional timer inside the speaker notes, you need to serve -the slides (see next paragraph forß details). +the slides (see next paragraph for details). If this does not work, you can also try start a server as part of your nbconvert command. To do this we use the ``ServePostProcessor``, which we activate by From 39dc25dc01f3f7400038fdc49307cb80000b4fcd Mon Sep 17 00:00:00 2001 From: M Pacer Date: Mon, 29 Jan 2018 15:06:59 -0800 Subject: [PATCH 091/671] add section for the timer and https server --- docs/source/usage.rst | 27 +++++++++++++++++++-------- 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/docs/source/usage.rst b/docs/source/usage.rst index 616861451..5dd414486 100644 --- a/docs/source/usage.rst +++ b/docs/source/usage.rst @@ -146,16 +146,27 @@ the ``--reveal-prefix`` command line flag to point to the local copy. This will create file ``your_talk.slides.html``, which you should be able to access with ``open your_talk.slides.html``. To access the speaker notes, press -``s`` after the slides load and they should open in a new window. Keep in mind -that if you want a functional timer inside the speaker notes, you need to serve -the slides (see next paragraph for details). +``s`` after the slides load and they should open in a new window. -If this does not work, you can also try start a server as part of your nbconvert -command. To do this we use the ``ServePostProcessor``, which we activate by -appending the command line flag ``--post serve`` to the above command. This -will not allow you to use speaker notes if you do not have a local copy of -reveal.js. +Serving slides with an https server: ``--post serve`` +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Once you have speaker notes working you may notice that your timers don't work. +Timers require a bit more infrastructure; you need to serve your local copy of +reveal.js from a local https server. + +Fortunately, ``nbconvert`` makes this fairly straightforward through the use of +the ``ServePostProcessor``. To activate this server, we append the command line +flag ``--post serve`` to our call to nbconvert. + +.. code-block:: shell + + jupyter nbconvert your_talk.ipynb --to slides --reveal-prefix reveal.js --post serve + +This will run the server, which will occupy the terminal that you ran the +command in until you stop it. You can stop the server by pressing ``ctrl C`` +twice. .. _convert_markdown: From e60dd481eaaec3aaf751b282c3febbc0a04f3741 Mon Sep 17 00:00:00 2001 From: M Pacer Date: Mon, 29 Jan 2018 15:08:11 -0800 Subject: [PATCH 092/671] rework layout of the sections, include links, maintain single narrative --- docs/source/usage.rst | 34 ++++++++++++++++++++-------------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/docs/source/usage.rst b/docs/source/usage.rst index 5dd414486..553344806 100644 --- a/docs/source/usage.rst +++ b/docs/source/usage.rst @@ -96,14 +96,20 @@ Reveal.js HTML slideshow Running this slideshow requires a copy of reveal.js (version 3.x). By default, this will include a script tag in the html that will directly load -reveal.js from a CDN. +reveal.js from a public CDN. -However, some features (specifically, speaker notes) are only available if you -use a local copy of reveal.js. This requires that first you have a local copy -of reveal.js and then that you redirect the script away from your CDN to your -local copy. +This means that if you include your slides on a webpage, they should work as +expected. However, some features (specifically, speaker notes & timers) will not +work on website because they require access to a local copy of reveal.js. -To make this clearer, let's look at an example. +Speaker notes require a local copy of reveal.js. Then, you need to tell +``nbconvert`` how to find that local copy. + +Timers only work if you already have speaker notes, but also require a local +https server. You can read more about this in ServePostProcessorExample_. + +To make this clearer, let's look at an example of how to get speaker notes +working with a local copy of reveal.js: SlidesWithNotesExample_. .. note:: @@ -115,6 +121,8 @@ To make this clearer, let's look at an example. cells designated as "skip" will not be included, "notes" will be included only in presenter notes, etc. +.. _SlidesWithNotesExample: + Example: creating slides w/ speaker notes ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -123,8 +131,8 @@ to slides. For this example, we'll assume that you are working in the same directory as the notebook you want to convert (i.e., when you run ``ls .``, ``your_talk.ipynb`` shows up amongst the list of files). -First, we need a compatible version of reveal.js in the current folder run the -following commands inside the directory: +First, we need a copy of reveal.js in the same directory as your slides. One +way to do this is to use the following commands in your terminal: .. code-block:: shell @@ -133,11 +141,7 @@ following commands inside the directory: git checkout 3.5.0 cd .. -Alternative, you can download a zip (or tar.gz) file containing reveal.js from -https://github.com/hakimel/reveal.js/releases/tag/3.5.0, but be sure to unzip -(or untar) the file to a directory named reveal.js. - -Then we need to tell nbconvert to point to this local copy. To do that we use +Then we need to tell nbconvert to point to this local copy. To do that we use the ``--reveal-prefix`` command line flag to point to the local copy. .. code-block:: shell @@ -146,8 +150,10 @@ the ``--reveal-prefix`` command line flag to point to the local copy. This will create file ``your_talk.slides.html``, which you should be able to access with ``open your_talk.slides.html``. To access the speaker notes, press -``s`` after the slides load and they should open in a new window. +``s`` after the slides load and they should open in a new window. + +.. _ServePostProcessorExample: Serving slides with an https server: ``--post serve`` ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ From 65362d73d2ad277746fd0c6743fa0d4200089bc5 Mon Sep 17 00:00:00 2001 From: M Pacer Date: Mon, 29 Jan 2018 15:08:33 -0800 Subject: [PATCH 093/671] include caveat about it not working completely offline --- docs/source/usage.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/source/usage.rst b/docs/source/usage.rst index 553344806..d7b7d5eb3 100644 --- a/docs/source/usage.rst +++ b/docs/source/usage.rst @@ -152,6 +152,9 @@ This will create file ``your_talk.slides.html``, which you should be able to access with ``open your_talk.slides.html``. To access the speaker notes, press ``s`` after the slides load and they should open in a new window. +Note: a local copy of reveal.js does allow slides that will run completely +offline, because the slides rely on other resources (e.g., mathjax and jquery) +that still require access to public CDNs (by default). .. _ServePostProcessorExample: From 6a454baf645919a147981a86bfd4a6efb3317e52 Mon Sep 17 00:00:00 2001 From: M Pacer Date: Mon, 29 Jan 2018 15:10:11 -0800 Subject: [PATCH 094/671] remove config_options.rst in make clean (fixes bug in autogeneration) I found that if the config_options.rst file was present, the make html was not recreating them, leading to out of date configuration docs. --- docs/Makefile | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/Makefile b/docs/Makefile index a457194e6..4507ba3da 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -50,6 +50,7 @@ help: clean: rm -rf $(BUILDDIR)/* + rm source/config_options.rst html: source/config_options.rst $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html From 9a8a9dea6927f48791143da3288f05b4fe87e6d7 Mon Sep 17 00:00:00 2001 From: M Pacer Date: Tue, 30 Jan 2018 18:01:05 -0800 Subject: [PATCH 095/671] clarify the issues with running offline --- docs/source/usage.rst | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/source/usage.rst b/docs/source/usage.rst index d7b7d5eb3..045f30c3c 100644 --- a/docs/source/usage.rst +++ b/docs/source/usage.rst @@ -152,9 +152,10 @@ This will create file ``your_talk.slides.html``, which you should be able to access with ``open your_talk.slides.html``. To access the speaker notes, press ``s`` after the slides load and they should open in a new window. -Note: a local copy of reveal.js does allow slides that will run completely -offline, because the slides rely on other resources (e.g., mathjax and jquery) -that still require access to public CDNs (by default). +Note: This does not enable slides that run completely offline. While you have a +local copy of reveal.js, by default, the slides need to access mathjax, require, +and jquery via a public CDN. Addressing this use case is an open issue and `PRs +`_ are always encouraged. .. _ServePostProcessorExample: From 59f5774786d5e9814b07e1588f09f35d7fdecc93 Mon Sep 17 00:00:00 2001 From: M Pacer Date: Tue, 30 Jan 2018 18:02:22 -0800 Subject: [PATCH 096/671] fix inconsistent header level from customising.ipynb --- docs/source/customizing.ipynb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/customizing.ipynb b/docs/source/customizing.ipynb index da35584a8..f8ea76a50 100644 --- a/docs/source/customizing.ipynb +++ b/docs/source/customizing.ipynb @@ -18,7 +18,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "### Converting a notebook to an (I)Python script and printing to stdout\n", + "## Converting a notebook to an (I)Python script and printing to stdout\n", "\n", "Out of the box, nbconvert can be used to convert notebooks to plain Python files. For example, the following command converts the `example.ipynb` notebook to Python and prints out the result:" ] From f4a23f80f75f34c2f4785998c644d40be1d272f4 Mon Sep 17 00:00:00 2001 From: Damian Avila Date: Wed, 31 Jan 2018 19:11:14 -0300 Subject: [PATCH 097/671] Update notebook css to 5.4.0 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 8fb3f373f..66ba939ec 100644 --- a/setup.py +++ b/setup.py @@ -66,7 +66,7 @@ } -notebook_css_version = '5.1.0' +notebook_css_version = '5.4.0' css_url = "https://cdn.jupyter.org/notebook/%s/style/style.min.css" % notebook_css_version class FetchCSS(Command): From bc39f45ba4f5111be3efe824629d74c1f05cd368 Mon Sep 17 00:00:00 2001 From: Matthias Geier Date: Mon, 23 Oct 2017 14:12:06 +0200 Subject: [PATCH 098/671] Enable ANSI underline and inverse See https://github.com/jupyter/notebook/pull/2967. --- nbconvert/filters/ansi.py | 49 ++++++++++++++++++++++++++++++--------- 1 file changed, 38 insertions(+), 11 deletions(-) diff --git a/nbconvert/filters/ansi.py b/nbconvert/filters/ansi.py index 7f7c62279..0a9b1ae49 100644 --- a/nbconvert/filters/ansi.py +++ b/nbconvert/filters/ansi.py @@ -74,12 +74,12 @@ def ansi2latex(text): return _ansi2anything(text, _latexconverter) -def _htmlconverter(fg, bg, bold): +def _htmlconverter(fg, bg, bold, underline): """ - Return start and end tags for given foreground/background/bold. + Return start and end tags for given foreground/background/bold/underline. """ - if (fg, bg, bold) == (None, None, False): + if (fg, bg, bold, underline) == (None, None, False, False): return '', '' classes = [] @@ -98,6 +98,9 @@ def _htmlconverter(fg, bg, bold): if bold: classes.append('ansi-bold') + if underline: + classes.append('ansi-underline') + starttag = '' -def _latexconverter(fg, bg, bold): +def _latexconverter(fg, bg, bold, underline): """ - Return start and end markup given foreground/background/bold. + Return start and end markup given foreground/background/bold/underline. """ - if (fg, bg, bold) == (None, None, False): + if (fg, bg, bold, underline) == (None, None, False, False): return '', '' starttag, endtag = '', '' @@ -140,6 +143,11 @@ def _latexconverter(fg, bg, bold): if bold: starttag += r'\textbf{' endtag = '}' + endtag + + if underline: + starttag += r'\underline{' + endtag = '}' + endtag + return starttag, endtag @@ -152,11 +160,13 @@ def _ansi2anything(text, converter): Accepts codes like '\x1b[32m' (red) and '\x1b[1;32m' (bold, red). The codes 1 (bold) and 5 (blinking) are selecting a bold font, code 0 and an empty code ('\x1b[m') reset colors and bold-ness. - Unlike in most terminals, "bold" doesn't change the color. The codes 21 and 22 deselect "bold", the codes 39 and 49 deselect the foreground and background color, respectively. The codes 38 and 48 select the "extended" set of foreground and background colors, respectively. + The code 4 underlines the following text and the code 7 inverts + foreground and background. The codes 24 and 27 switch the underline + and inversion off, respectively. Non-color escape sequences (not ending with 'm') are filtered out. @@ -166,6 +176,8 @@ def _ansi2anything(text, converter): """ fg, bg = None, None bold = False + underline = False + inverse = False numbers = [] out = [] @@ -185,9 +197,16 @@ def _ansi2anything(text, converter): chunk, text = text, '' if chunk: - if bold and fg in range(8): - fg += 8 - starttag, endtag = converter(fg, bg, bold) + chunk_fg, chunk_bg = fg, bg + if bold and chunk_fg in range(8): + chunk_fg += 8 + if inverse: + if chunk_fg is None: + chunk_fg = 0, 0, 0 + if chunk_bg is None: + chunk_bg = 255, 255, 255 + chunk_fg, chunk_bg = chunk_bg, chunk_fg + starttag, endtag = converter(chunk_fg, chunk_bg, bold, underline) out.append(starttag) out.append(chunk) out.append(endtag) @@ -196,11 +215,19 @@ def _ansi2anything(text, converter): n = numbers.pop(0) if n == 0: fg = bg = None - bold = False + bold = underline = inverse = False elif n in (1, 5): bold = True + elif n == 4: + underline = True + elif n == 7: + inverse = True elif n in (21, 22): bold = False + elif n == 24: + underline = False + elif n == 27: + inverse = False elif 30 <= n <= 37: fg = n - 30 elif n == 38: From e8215f2250b1e28baf14a786326e299aba6f1ad4 Mon Sep 17 00:00:00 2001 From: Matthias Geier Date: Thu, 2 Nov 2017 10:26:26 +0100 Subject: [PATCH 099/671] Overridable default colors for ANSI "inverse" --- nbconvert/filters/ansi.py | 42 ++++++++++++++++++----------- nbconvert/templates/latex/base.tplx | 2 ++ 2 files changed, 28 insertions(+), 16 deletions(-) diff --git a/nbconvert/filters/ansi.py b/nbconvert/filters/ansi.py index 0a9b1ae49..d00a308d8 100644 --- a/nbconvert/filters/ansi.py +++ b/nbconvert/filters/ansi.py @@ -74,26 +74,33 @@ def ansi2latex(text): return _ansi2anything(text, _latexconverter) -def _htmlconverter(fg, bg, bold, underline): +def _htmlconverter(fg, bg, bold, underline, inverse): """ Return start and end tags for given foreground/background/bold/underline. """ - if (fg, bg, bold, underline) == (None, None, False, False): + if (fg, bg, bold, underline, inverse) == (None, None, False, False, False): return '', '' classes = [] styles = [] + if inverse: + fg, bg = bg, fg + if isinstance(fg, int): classes.append(_ANSI_COLORS[fg] + '-fg') elif fg: styles.append('color: rgb({},{},{})'.format(*fg)) + elif inverse: + classes.append('ansi-default-inverse-fg') if isinstance(bg, int): classes.append(_ANSI_COLORS[bg] + '-bg') elif bg: styles.append('background-color: rgb({},{},{})'.format(*bg)) + elif inverse: + classes.append('ansi-default-inverse-bg') if bold: classes.append('ansi-bold') @@ -110,16 +117,19 @@ def _htmlconverter(fg, bg, bold, underline): return starttag, '' -def _latexconverter(fg, bg, bold, underline): +def _latexconverter(fg, bg, bold, underline, inverse): """ Return start and end markup given foreground/background/bold/underline. """ - if (fg, bg, bold, underline) == (None, None, False, False): + if (fg, bg, bold, underline, inverse) == (None, None, False, False, False): return '', '' starttag, endtag = '', '' + if inverse: + fg, bg = bg, fg + if isinstance(fg, int): starttag += r'\textcolor{' + _ANSI_COLORS[fg] + '}{' endtag = '}' + endtag @@ -128,10 +138,13 @@ def _latexconverter(fg, bg, bold, underline): starttag += r'\def\tcRGB{\textcolor[RGB]}\expandafter' starttag += r'\tcRGB\expandafter{\detokenize{%s,%s,%s}}{' % fg endtag = '}' + endtag + elif inverse: + starttag += r'\textcolor{ansi-default-inverse-fg}{' + endtag = '}' + endtag if isinstance(bg, int): - starttag += r'\setlength{\fboxsep}{0pt}\colorbox{' - starttag += _ANSI_COLORS[bg] + '}{' + starttag += r'\setlength{\fboxsep}{0pt}' + starttag += r'\colorbox{' + _ANSI_COLORS[bg] + '}{' endtag = r'\strut}' + endtag elif bg: starttag += r'\setlength{\fboxsep}{0pt}' @@ -139,6 +152,10 @@ def _latexconverter(fg, bg, bold, underline): starttag += r'\def\cbRGB{\colorbox[RGB]}\expandafter' starttag += r'\cbRGB\expandafter{\detokenize{%s,%s,%s}}{' % bg endtag = r'\strut}' + endtag + elif inverse: + starttag += r'\setlength{\fboxsep}{0pt}' + starttag += r'\colorbox{ansi-default-inverse-bg}{' + endtag = r'\strut}' + endtag if bold: starttag += r'\textbf{' @@ -197,16 +214,9 @@ def _ansi2anything(text, converter): chunk, text = text, '' if chunk: - chunk_fg, chunk_bg = fg, bg - if bold and chunk_fg in range(8): - chunk_fg += 8 - if inverse: - if chunk_fg is None: - chunk_fg = 0, 0, 0 - if chunk_bg is None: - chunk_bg = 255, 255, 255 - chunk_fg, chunk_bg = chunk_bg, chunk_fg - starttag, endtag = converter(chunk_fg, chunk_bg, bold, underline) + starttag, endtag = converter( + fg + 8 if bold and fg in range(8) else fg, + bg, bold, underline, inverse) out.append(starttag) out.append(chunk) out.append(endtag) diff --git a/nbconvert/templates/latex/base.tplx b/nbconvert/templates/latex/base.tplx index 0582eb657..aaed836ce 100644 --- a/nbconvert/templates/latex/base.tplx +++ b/nbconvert/templates/latex/base.tplx @@ -89,6 +89,8 @@ This template does not define a docclass, the inheriting class must define this. \definecolor{ansi-cyan-intense}{HTML}{258F8F} \definecolor{ansi-white}{HTML}{C5C1B4} \definecolor{ansi-white-intense}{HTML}{A1A6B2} + \definecolor{ansi-default-inverse-fg}{HTML}{FFFFFF} + \definecolor{ansi-default-inverse-bg}{HTML}{000000} % commands and environments needed by pandoc snippets % extracted from the output of `pandoc -s` From 5c18e5db0fcb75a1bd39c130973f727de2518a4d Mon Sep 17 00:00:00 2001 From: Matthias Geier Date: Fri, 5 Jan 2018 16:25:05 +0100 Subject: [PATCH 100/671] ANSI conversion: shorten overly verbose docstring --- nbconvert/filters/ansi.py | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/nbconvert/filters/ansi.py b/nbconvert/filters/ansi.py index d00a308d8..55bc47acb 100644 --- a/nbconvert/filters/ansi.py +++ b/nbconvert/filters/ansi.py @@ -175,15 +175,6 @@ def _ansi2anything(text, converter): See https://en.wikipedia.org/wiki/ANSI_escape_code Accepts codes like '\x1b[32m' (red) and '\x1b[1;32m' (bold, red). - The codes 1 (bold) and 5 (blinking) are selecting a bold font, code - 0 and an empty code ('\x1b[m') reset colors and bold-ness. - The codes 21 and 22 deselect "bold", the codes 39 and 49 deselect - the foreground and background color, respectively. - The codes 38 and 48 select the "extended" set of foreground and - background colors, respectively. - The code 4 underlines the following text and the code 7 inverts - foreground and background. The codes 24 and 27 switch the underline - and inversion off, respectively. Non-color escape sequences (not ending with 'm') are filtered out. @@ -203,6 +194,7 @@ def _ansi2anything(text, converter): if m: if m.group(2) == 'm': try: + # Empty code is same as code 0 numbers = [int(n) if n else 0 for n in m.group(1).split(';')] except ValueError: @@ -224,12 +216,16 @@ def _ansi2anything(text, converter): while numbers: n = numbers.pop(0) if n == 0: + # Code 0 (same as empty code): reset everything fg = bg = None bold = underline = inverse = False - elif n in (1, 5): + elif n == 1: bold = True elif n == 4: underline = True + elif n == 5: + # Code 5: blinking + bold = True elif n == 7: inverse = True elif n in (21, 22): From 4bc6c2e92f6b0913c343b38c3984e80c51a94e96 Mon Sep 17 00:00:00 2001 From: Matthias Geier Date: Fri, 5 Jan 2018 17:09:50 +0100 Subject: [PATCH 101/671] TST: Add a few test cases for ANSI conversion --- nbconvert/filters/tests/test_ansi.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/nbconvert/filters/tests/test_ansi.py b/nbconvert/filters/tests/test_ansi.py index 707d48211..ba6ae25f2 100644 --- a/nbconvert/filters/tests/test_ansi.py +++ b/nbconvert/filters/tests/test_ansi.py @@ -41,6 +41,9 @@ def test_ansi2html(self): 'hel\x1b[0;32mlo': 'hello', 'hellø': 'hellø', '\x1b[1mhello\x1b[33mworld\x1b[0m': 'helloworld', + 'he\x1b[4mll\x1b[24mo': 'hello', + '\x1b[35mhe\x1b[7mll\x1b[27mo': 'hello', + '\x1b[44mhe\x1b[7mll\x1b[27mo': 'hello', } for inval, outval in correct_outputs.items(): @@ -61,6 +64,7 @@ def test_ansi2latex(self): 'hello\x1b[01;34mthere': r'hello\textcolor{ansi-blue-intense}{\textbf{there}}', 'hello\x1b[001;34mthere': r'hello\textcolor{ansi-blue-intense}{\textbf{there}}', '\x1b[1mhello\x1b[33mworld\x1b[0m': r'\textbf{hello}\textcolor{ansi-yellow-intense}{\textbf{world}}', + 'he\x1b[4mll\x1b[24mo': 'he\\underline{ll}o', } for inval, outval in correct_outputs.items(): From dab959638199ce562534efcdd9a9d7eb9dfc31bd Mon Sep 17 00:00:00 2001 From: Matthias Geier Date: Fri, 2 Feb 2018 11:41:45 +0100 Subject: [PATCH 102/671] TST: Add test cases for ANSI inverse with LaTeX output --- nbconvert/filters/tests/test_ansi.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/nbconvert/filters/tests/test_ansi.py b/nbconvert/filters/tests/test_ansi.py index ba6ae25f2..53d48569b 100644 --- a/nbconvert/filters/tests/test_ansi.py +++ b/nbconvert/filters/tests/test_ansi.py @@ -65,6 +65,8 @@ def test_ansi2latex(self): 'hello\x1b[001;34mthere': r'hello\textcolor{ansi-blue-intense}{\textbf{there}}', '\x1b[1mhello\x1b[33mworld\x1b[0m': r'\textbf{hello}\textcolor{ansi-yellow-intense}{\textbf{world}}', 'he\x1b[4mll\x1b[24mo': 'he\\underline{ll}o', + '\x1b[35mhe\x1b[7mll\x1b[27mo': r'\textcolor{ansi-magenta}{he}\textcolor{ansi-default-inverse-fg}{\setlength{\fboxsep}{0pt}\colorbox{ansi-magenta}{ll\strut}}\textcolor{ansi-magenta}{o}', + '\x1b[44mhe\x1b[7mll\x1b[27mo': r'\setlength{\fboxsep}{0pt}\colorbox{ansi-blue}{he\strut}\textcolor{ansi-blue}{\setlength{\fboxsep}{0pt}\colorbox{ansi-default-inverse-bg}{ll\strut}}\setlength{\fboxsep}{0pt}\colorbox{ansi-blue}{o\strut}', } for inval, outval in correct_outputs.items(): From 0a55f1b1e4ee005b9638dca31beee0c100861b7f Mon Sep 17 00:00:00 2001 From: Thomas Kluyver Date: Fri, 2 Feb 2018 11:09:04 +0000 Subject: [PATCH 103/671] Include apt command inline --- docs/source/install.rst | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/source/install.rst b/docs/source/install.rst index d4dfc1536..88e71b296 100644 --- a/docs/source/install.rst +++ b/docs/source/install.rst @@ -57,7 +57,9 @@ Fortunately, there are packages that make this much easier. These packages are specific to different operating systems: * Linux: `TeX Live `_ - * For Installing Xelatex on Ubuntu: https://tex.stackexchange.com/questions/179778/xelatex-under-ubuntu + + * E.g. on Debian or Ubuntu: ``sudo apt-get install texlive-xetex`` + * macOS (OS X): `MacTeX `_. * Windows: `MikTex `_ From 342b05c09a76d81146f82f31cd8199bf1124d83f Mon Sep 17 00:00:00 2001 From: Hagai Hargil Date: Wed, 7 Feb 2018 10:25:50 +0200 Subject: [PATCH 104/671] Windows unicode error fixed, nosetest added to setup.py --- nbconvert/preprocessors/csshtmlheader.py | 5 ++--- setup.py | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/nbconvert/preprocessors/csshtmlheader.py b/nbconvert/preprocessors/csshtmlheader.py index db5430ab8..7d4372978 100755 --- a/nbconvert/preprocessors/csshtmlheader.py +++ b/nbconvert/preprocessors/csshtmlheader.py @@ -10,7 +10,6 @@ import nbconvert.resources from traitlets import Unicode -from ipython_genutils.py3compat import str_to_bytes from .base import Preprocessor @@ -132,6 +131,6 @@ def _generate_header(self, resources): def _hash(self, filename): """Compute the hash of a file.""" md5 = hashlib.md5() - with open(filename) as f: - md5.update(str_to_bytes(f.read())) + with open(filename, 'rb') as f: + md5.update(f.read()) return md5.digest() diff --git a/setup.py b/setup.py index 66ba939ec..988cc30c6 100644 --- a/setup.py +++ b/setup.py @@ -199,7 +199,7 @@ def run(self): ] extra_requirements = { - 'test': ['pytest', 'pytest-cov', 'ipykernel', 'jupyter_client>=4.2'], + 'test': ['pytest', 'pytest-cov', 'ipykernel', 'jupyter_client>=4.2', 'nose'], 'serve': ['tornado>=4.0'], 'execute': ['jupyter_client>=4.2'], } From cb3cc9b38467846e3ff19f1f2bf4c03ab218a51c Mon Sep 17 00:00:00 2001 From: M Pacer Date: Thu, 8 Feb 2018 03:04:09 -0800 Subject: [PATCH 105/671] remove dependency on ipython_genutils, thereby plucking one nose hair --- nbconvert/utils/tests/test_io.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/nbconvert/utils/tests/test_io.py b/nbconvert/utils/tests/test_io.py index ba23bf71b..cb80275bf 100644 --- a/nbconvert/utils/tests/test_io.py +++ b/nbconvert/utils/tests/test_io.py @@ -7,7 +7,8 @@ import io as stdlib_io import sys -from ipython_genutils.testing.decorators import skipif +import pytest + from ..io import unicode_std_stream from ipython_genutils.py3compat import PY3 @@ -36,7 +37,8 @@ def test_UnicodeStdStream(): finally: sys.stdout = orig_stdout -@skipif(not PY3, "Not applicable on Python 2") +@pytest.mark.skipif(not PY3, + reason = "Not applicable on Python 2") def test_UnicodeStdStream_nowrap(): # If we replace stdout with a StringIO, it shouldn't get wrapped. orig_stdout = sys.stdout From ad833ea5e35d27bf97dfd106f136763e43dffded Mon Sep 17 00:00:00 2001 From: M Pacer Date: Thu, 8 Feb 2018 03:39:04 -0800 Subject: [PATCH 106/671] port ipython_genutils.onlyif_cmds_exist, replacing nose with pytest Now we should be almost nose free. --- nbconvert/preprocessors/tests/test_svg2pdf.py | 6 ++++-- nbconvert/utils/io.py | 14 +++++++++++++- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/nbconvert/preprocessors/tests/test_svg2pdf.py b/nbconvert/preprocessors/tests/test_svg2pdf.py index b937cf58a..23b3529d2 100644 --- a/nbconvert/preprocessors/tests/test_svg2pdf.py +++ b/nbconvert/preprocessors/tests/test_svg2pdf.py @@ -3,11 +3,12 @@ # Copyright (c) IPython Development Team. # Distributed under the terms of the Modified BSD License. -from ipython_genutils.testing import decorators as dec +from ipython_genutils.py3compat import which, PY3 from nbformat import v4 as nbformat from .base import PreprocessorTestsBase from ..svg2pdf import SVG2PDFPreprocessor +from ...utils.io import onlyif_cmds_exist class Testsvg2pdf(PreprocessorTestsBase): @@ -63,7 +64,7 @@ def test_constructor(self): self.build_preprocessor() - @dec.onlyif_cmds_exist('inkscape') + @onlyif_cmds_exist('inkscape') def test_output(self): """Test the output of the SVG2PDFPreprocessor""" nb = self.build_notebook() @@ -71,3 +72,4 @@ def test_output(self): preprocessor = self.build_preprocessor() nb, res = preprocessor(nb, res) self.assertIn('application/pdf', nb.cells[0].outputs[0].data) + diff --git a/nbconvert/utils/io.py b/nbconvert/utils/io.py index e05347009..703014422 100644 --- a/nbconvert/utils/io.py +++ b/nbconvert/utils/io.py @@ -6,8 +6,10 @@ import codecs import sys -from ipython_genutils.py3compat import PY3 +import pytest + +from ipython_genutils.py3compat import PY3, which def unicode_std_stream(stream='stdout'): u"""Get a wrapper to write unicode to stdout/stderr as UTF-8. @@ -51,3 +53,13 @@ def unicode_stdin_stream(): stream_b = stream return codecs.getreader('utf-8')(stream_b) + +def onlyif_cmds_exist(*commands): + """ + Decorator to skip test when at least one of `commands` is not found. + """ + for cmd in commands: + if not which(cmd): + return pytest.mark.skip("This test runs only if command '{0}' " + "is installed".format(cmd)) + return lambda f: f From 631d178e827790af081de0f4f6612998fbb457cc Mon Sep 17 00:00:00 2001 From: M Pacer Date: Thu, 8 Feb 2018 03:47:16 -0800 Subject: [PATCH 107/671] swap all other instances of onlyif_cmds_exist --- nbconvert/exporters/tests/test_asciidoc.py | 6 ++--- nbconvert/exporters/tests/test_latex.py | 2 +- nbconvert/exporters/tests/test_pdf.py | 6 ++--- nbconvert/exporters/tests/test_rst.py | 2 +- nbconvert/filters/tests/test_markdown.py | 11 ++++---- nbconvert/tests/test_nbconvertapp.py | 29 +++++++++++----------- nbconvert/utils/tests/test_pandoc.py | 6 ++--- 7 files changed, 32 insertions(+), 30 deletions(-) diff --git a/nbconvert/exporters/tests/test_asciidoc.py b/nbconvert/exporters/tests/test_asciidoc.py index 9fb20be47..f005d29c9 100644 --- a/nbconvert/exporters/tests/test_asciidoc.py +++ b/nbconvert/exporters/tests/test_asciidoc.py @@ -15,10 +15,10 @@ import re from traitlets.config import Config -from ipython_genutils.testing import decorators as dec from .base import ExportersTestsBase from ..asciidoc import ASCIIDocExporter +from ...utils.io import onlyif_cmds_exist #----------------------------------------------------------------------------- # Class @@ -38,7 +38,7 @@ def test_constructor(self): ASCIIDocExporter() - @dec.onlyif_cmds_exist('pandoc') + @onlyif_cmds_exist('pandoc') def test_export(self): """ Can a ASCIIDocExporter export something? @@ -49,7 +49,7 @@ def test_export(self): assert re.findall(in_regex, output) assert re.findall(out_regex, output) - @dec.onlyif_cmds_exist('pandoc') + @onlyif_cmds_exist('pandoc') def test_export_no_prompt(self): """ Can a ASCIIDocExporter export something without prompts? diff --git a/nbconvert/exporters/tests/test_latex.py b/nbconvert/exporters/tests/test_latex.py index b58273293..3ef394265 100644 --- a/nbconvert/exporters/tests/test_latex.py +++ b/nbconvert/exporters/tests/test_latex.py @@ -9,11 +9,11 @@ from .base import ExportersTestsBase from ..latex import LatexExporter +from ...utils.io import onlyif_cmds_exist from traitlets.config import Config from nbformat import write from nbformat import v4 -from ipython_genutils.testing.decorators import onlyif_cmds_exist from testpath.tempdir import TemporaryDirectory from jinja2 import DictLoader diff --git a/nbconvert/exporters/tests/test_pdf.py b/nbconvert/exporters/tests/test_pdf.py index d79b95126..bd8254df1 100644 --- a/nbconvert/exporters/tests/test_pdf.py +++ b/nbconvert/exporters/tests/test_pdf.py @@ -7,11 +7,11 @@ import os import shutil -from ipython_genutils.testing import decorators as dec from testpath import tempdir from .base import ExportersTestsBase from ..pdf import PDFExporter +from ...utils.io import onlyif_cmds_exist #----------------------------------------------------------------------------- @@ -28,8 +28,8 @@ def test_constructor(self): self.exporter_class() - @dec.onlyif_cmds_exist('xelatex') - @dec.onlyif_cmds_exist('pandoc') + @onlyif_cmds_exist('xelatex') + @onlyif_cmds_exist('pandoc') def test_export(self): """Smoke test PDFExporter""" with tempdir.TemporaryDirectory() as td: diff --git a/nbconvert/exporters/tests/test_rst.py b/nbconvert/exporters/tests/test_rst.py index 6119bfa85..12ad24a5b 100644 --- a/nbconvert/exporters/tests/test_rst.py +++ b/nbconvert/exporters/tests/test_rst.py @@ -11,7 +11,7 @@ from .base import ExportersTestsBase from ..rst import RSTExporter -from ipython_genutils.testing.decorators import onlyif_cmds_exist +from ...utils.io import onlyif_cmds_exist class TestRSTExporter(ExportersTestsBase): diff --git a/nbconvert/filters/tests/test_markdown.py b/nbconvert/filters/tests/test_markdown.py index 6e3518976..1e3a1d6c5 100644 --- a/nbconvert/filters/tests/test_markdown.py +++ b/nbconvert/filters/tests/test_markdown.py @@ -8,9 +8,10 @@ from copy import copy from functools import partial + from ipython_genutils.py3compat import string_types -from ipython_genutils.testing import decorators as dec +from ...utils.io import onlyif_cmds_exist from ...tests.base import TestsBase from ..pandoc import convert_pandoc from ..markdown import markdown2html @@ -48,7 +49,7 @@ class TestMarkdown(TestsBase): ('test', 'https://google.com/'), ] - @dec.onlyif_cmds_exist('pandoc') + @onlyif_cmds_exist('pandoc') def test_markdown2latex(self): """markdown2latex test""" for index, test in enumerate(self.tests): @@ -57,7 +58,7 @@ def test_markdown2latex(self): convert_pandoc, from_format='markdown', to_format='latex'), test, self.tokens[index]) - @dec.onlyif_cmds_exist('pandoc') + @onlyif_cmds_exist('pandoc') def test_markdown2latex_markup(self): """markdown2latex with markup kwarg test""" # This string should be passed through unaltered with pandoc's @@ -79,7 +80,7 @@ def test_markdown2latex_markup(self): convert_pandoc(s, 'markdown_strict+tex_math_dollars', 'latex'), expected) - @dec.onlyif_cmds_exist('pandoc') + @onlyif_cmds_exist('pandoc') def test_pandoc_extra_args(self): # pass --no-wrap s = '\n'.join([ @@ -227,7 +228,7 @@ def test_markdown2html_math_paragraph(self): s = markdown2html(case) self.assertIn(case, self._unescape(s)) - @dec.onlyif_cmds_exist('pandoc') + @onlyif_cmds_exist('pandoc') def test_markdown2rst(self): """markdown2rst test""" diff --git a/nbconvert/tests/test_nbconvertapp.py b/nbconvert/tests/test_nbconvertapp.py index bdd392949..babc5e797 100644 --- a/nbconvert/tests/test_nbconvertapp.py +++ b/nbconvert/tests/test_nbconvertapp.py @@ -9,11 +9,12 @@ from .base import TestsBase from ..postprocessors import PostProcessorBase +from ..utils.io import onlyif_cmds_exist from nbconvert import nbconvertapp from nbconvert.exporters import Exporter + from traitlets.tests.utils import check_help_all_output -from ipython_genutils.testing import decorators as dec from testpath import tempdir import pytest @@ -117,8 +118,8 @@ def test_relative_template_file(self): text = f.read() assert text == test_output - @dec.onlyif_cmds_exist('xelatex') - @dec.onlyif_cmds_exist('pandoc') + @onlyif_cmds_exist('xelatex') + @onlyif_cmds_exist('pandoc') def test_filename_spaces(self): """ Generate PDFs with graphics if notebooks have spaces in the name? @@ -133,8 +134,8 @@ def test_filename_spaces(self): assert os.path.isfile('notebook with spaces.pdf') - @dec.onlyif_cmds_exist('xelatex') - @dec.onlyif_cmds_exist('pandoc') + @onlyif_cmds_exist('xelatex') + @onlyif_cmds_exist('pandoc') def test_pdf(self): """ Check to see if pdfs compile, even if strikethroughs are included. @@ -154,7 +155,7 @@ def test_post_processor(self): '--post nbconvert.tests.test_nbconvertapp.DummyPost') self.assertIn('Dummy:notebook1.py', out) - @dec.onlyif_cmds_exist('pandoc') + @onlyif_cmds_exist('pandoc') def test_spurious_cr(self): """Check for extra CR characters""" with self.create_temp_cwd(['notebook2.ipynb']): @@ -169,7 +170,7 @@ def test_spurious_cr(self): self.assertEqual(tex.count('\r'), tex.count('\r\n')) self.assertEqual(html.count('\r'), html.count('\r\n')) - @dec.onlyif_cmds_exist('pandoc') + @onlyif_cmds_exist('pandoc') def test_png_base64_html_ok(self): """Is embedded png data well formed in HTML?""" with self.create_temp_cwd(['notebook2.ipynb']): @@ -179,7 +180,7 @@ def test_png_base64_html_ok(self): with open('notebook2.html') as f: assert "data:image/png;base64,b'" not in f.read() - @dec.onlyif_cmds_exist('pandoc') + @onlyif_cmds_exist('pandoc') def test_template(self): """ Do export templates work? @@ -254,7 +255,7 @@ def test_accents_in_filename(self): self.nbconvert('--log-level 0 --to Python nb1_*') assert os.path.isfile(u'nb1_análisis.py') - @dec.onlyif_cmds_exist('xelatex', 'pandoc') + @onlyif_cmds_exist('xelatex', 'pandoc') def test_filename_accent_pdf(self): """ Generate PDFs if notebooks have an accent in their name? @@ -408,8 +409,8 @@ def test_convert_from_stdin(self): assert '```python' not in output1 # shouldn't have language assert "```" in output1 # but should have fenced blocks - @dec.onlyif_cmds_exist('xelatex') - @dec.onlyif_cmds_exist('pandoc') + @onlyif_cmds_exist('xelatex') + @onlyif_cmds_exist('pandoc') def test_linked_images(self): """ Generate PDFs with an image linked in a markdown cell @@ -418,7 +419,7 @@ def test_linked_images(self): self.nbconvert('--to pdf latex-linked-image.ipynb') assert os.path.isfile('latex-linked-image.pdf') - @dec.onlyif_cmds_exist('pandoc') + @onlyif_cmds_exist('pandoc') def test_embedded_jpeg(self): """ Verify that latex conversion succeeds @@ -429,7 +430,7 @@ def test_embedded_jpeg(self): self.nbconvert('--to latex notebook4_jpeg.ipynb') assert os.path.isfile('notebook4_jpeg.tex') - @dec.onlyif_cmds_exist('pandoc') + @onlyif_cmds_exist('pandoc') def test_markdown_display_priority(self): """ Check to see if markdown conversion embedds PNGs, @@ -444,7 +445,7 @@ def test_markdown_display_priority(self): assert ("markdown_display_priority_files/" "markdown_display_priority_0_1.png") in markdown_output - @dec.onlyif_cmds_exist('pandoc') + @onlyif_cmds_exist('pandoc') def test_write_figures_to_custom_path(self): """ Check if figure files are copied to configured path. diff --git a/nbconvert/utils/tests/test_pandoc.py b/nbconvert/utils/tests/test_pandoc.py index 9afcfa7be..9dcf74228 100644 --- a/nbconvert/utils/tests/test_pandoc.py +++ b/nbconvert/utils/tests/test_pandoc.py @@ -12,7 +12,7 @@ import os import warnings -from ipython_genutils.testing import decorators as dec +from ..io import onlyif_cmds_exist from nbconvert.tests.base import TestsBase from .. import pandoc @@ -27,7 +27,7 @@ def __init__(self, *args, **kwargs): super(TestPandoc, self).__init__(*args, **kwargs) self.original_env = os.environ.copy() - @dec.onlyif_cmds_exist('pandoc') + @onlyif_cmds_exist('pandoc') def test_pandoc_available(self): """ Test behaviour that pandoc functions raise PandocMissing as documented """ pandoc.clean_cache() @@ -48,7 +48,7 @@ def test_pandoc_available(self): pandoc.pandoc("", "markdown", "html") self.assertEqual(w, []) - @dec.onlyif_cmds_exist('pandoc') + @onlyif_cmds_exist('pandoc') def test_minimal_version(self): original_minversion = pandoc._minimal_version From 9c3fc49807bace5c6070f3fc885718542b4e99cd Mon Sep 17 00:00:00 2001 From: M Pacer Date: Thu, 8 Feb 2018 04:01:32 -0800 Subject: [PATCH 108/671] remove nose from dependencies & small cleanup --- nbconvert/filters/tests/test_markdown.py | 1 - nbconvert/preprocessors/tests/test_svg2pdf.py | 1 - setup.py | 2 +- 3 files changed, 1 insertion(+), 3 deletions(-) diff --git a/nbconvert/filters/tests/test_markdown.py b/nbconvert/filters/tests/test_markdown.py index 1e3a1d6c5..bfc332a76 100644 --- a/nbconvert/filters/tests/test_markdown.py +++ b/nbconvert/filters/tests/test_markdown.py @@ -8,7 +8,6 @@ from copy import copy from functools import partial - from ipython_genutils.py3compat import string_types from ...utils.io import onlyif_cmds_exist diff --git a/nbconvert/preprocessors/tests/test_svg2pdf.py b/nbconvert/preprocessors/tests/test_svg2pdf.py index 23b3529d2..9ead524ec 100644 --- a/nbconvert/preprocessors/tests/test_svg2pdf.py +++ b/nbconvert/preprocessors/tests/test_svg2pdf.py @@ -3,7 +3,6 @@ # Copyright (c) IPython Development Team. # Distributed under the terms of the Modified BSD License. -from ipython_genutils.py3compat import which, PY3 from nbformat import v4 as nbformat from .base import PreprocessorTestsBase diff --git a/setup.py b/setup.py index 988cc30c6..66ba939ec 100644 --- a/setup.py +++ b/setup.py @@ -199,7 +199,7 @@ def run(self): ] extra_requirements = { - 'test': ['pytest', 'pytest-cov', 'ipykernel', 'jupyter_client>=4.2', 'nose'], + 'test': ['pytest', 'pytest-cov', 'ipykernel', 'jupyter_client>=4.2'], 'serve': ['tornado>=4.0'], 'execute': ['jupyter_client>=4.2'], } From fcb6455a3f3fedb6632da3ab29437ce98d1b0ecf Mon Sep 17 00:00:00 2001 From: M Pacer Date: Thu, 8 Feb 2018 14:26:51 -0800 Subject: [PATCH 109/671] combine onlyif_cmds_exist calls into single calls --- nbconvert/exporters/tests/test_pdf.py | 3 +-- nbconvert/tests/test_nbconvertapp.py | 9 +++------ 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/nbconvert/exporters/tests/test_pdf.py b/nbconvert/exporters/tests/test_pdf.py index bd8254df1..e09ef21b8 100644 --- a/nbconvert/exporters/tests/test_pdf.py +++ b/nbconvert/exporters/tests/test_pdf.py @@ -28,8 +28,7 @@ def test_constructor(self): self.exporter_class() - @onlyif_cmds_exist('xelatex') - @onlyif_cmds_exist('pandoc') + @onlyif_cmds_exist('xelatex', 'pandoc') def test_export(self): """Smoke test PDFExporter""" with tempdir.TemporaryDirectory() as td: diff --git a/nbconvert/tests/test_nbconvertapp.py b/nbconvert/tests/test_nbconvertapp.py index babc5e797..4670331b7 100644 --- a/nbconvert/tests/test_nbconvertapp.py +++ b/nbconvert/tests/test_nbconvertapp.py @@ -118,8 +118,7 @@ def test_relative_template_file(self): text = f.read() assert text == test_output - @onlyif_cmds_exist('xelatex') - @onlyif_cmds_exist('pandoc') + @onlyif_cmds_exist('pandoc', 'xelatex') def test_filename_spaces(self): """ Generate PDFs with graphics if notebooks have spaces in the name? @@ -134,8 +133,7 @@ def test_filename_spaces(self): assert os.path.isfile('notebook with spaces.pdf') - @onlyif_cmds_exist('xelatex') - @onlyif_cmds_exist('pandoc') + @onlyif_cmds_exist('pandoc', 'xelatex') def test_pdf(self): """ Check to see if pdfs compile, even if strikethroughs are included. @@ -409,8 +407,7 @@ def test_convert_from_stdin(self): assert '```python' not in output1 # shouldn't have language assert "```" in output1 # but should have fenced blocks - @onlyif_cmds_exist('xelatex') - @onlyif_cmds_exist('pandoc') + @onlyif_cmds_exist('pandoc', 'xelatex') def test_linked_images(self): """ Generate PDFs with an image linked in a markdown cell From 95ba69487379c749354bbbfaa5452c3404ca8b53 Mon Sep 17 00:00:00 2001 From: Michael Droettboom Date: Fri, 9 Feb 2018 13:15:55 -0500 Subject: [PATCH 110/671] Add support for adding custom exporters to the "Download as" menu. --- docs/source/external_exporters.rst | 5 +++++ nbconvert/exporters/exporter.py | 4 ++++ 2 files changed, 9 insertions(+) diff --git a/docs/source/external_exporters.rst b/docs/source/external_exporters.rst index 5aa96bda6..f1111a3bf 100644 --- a/docs/source/external_exporters.rst +++ b/docs/source/external_exporters.rst @@ -175,6 +175,11 @@ We are going to write an exporter that: My custom exporter """ + # If this custom exporter should add an entry to the + # "File -> Download as" menu in the notebook, give it a name here in the + # `export_from_notebook` class member + export_from_notebook = "My format" + def _file_extension_default(self): """ The new file extension is `.test_ext` diff --git a/nbconvert/exporters/exporter.py b/nbconvert/exporters/exporter.py index e70868f91..3cc864563 100644 --- a/nbconvert/exporters/exporter.py +++ b/nbconvert/exporters/exporter.py @@ -61,6 +61,10 @@ class Exporter(LoggingConfigurable): # the class, not just on instances. output_mimetype = '' + # Should this converter be accessible from the notebook front-end? + # If so, should be a friendly name to display (and possibly translated). + export_from_notebook = None + #Configurability, allows the user to easily add filters and preprocessors. preprocessors = List( help="""List of preprocessors, by name or namespace, to enable.""" From 3f932374accf9099726f7943c775e32e0c942bb1 Mon Sep 17 00:00:00 2001 From: Saul Shanabrook Date: Mon, 26 Feb 2018 14:09:38 -0500 Subject: [PATCH 111/671] Test for cleaning up `notebook.tex` in pdf --- nbconvert/exporters/tests/test_pdf.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/nbconvert/exporters/tests/test_pdf.py b/nbconvert/exporters/tests/test_pdf.py index e09ef21b8..ca864eaf2 100644 --- a/nbconvert/exporters/tests/test_pdf.py +++ b/nbconvert/exporters/tests/test_pdf.py @@ -37,4 +37,6 @@ def test_export(self): (output, resources) = self.exporter_class(latex_count=1).from_filename(newpath) self.assertIsInstance(output, bytes) assert len(output) > 0 + # tex file should be cleaned up + assert 'notebook.tex' not in os.listdir(td) From 9ffb7533506886818198bc660334d492869e8c9b Mon Sep 17 00:00:00 2001 From: Saul Shanabrook Date: Mon, 26 Feb 2018 14:18:46 -0500 Subject: [PATCH 112/671] Write tex file in pdf generation to temp folder --- nbconvert/exporters/pdf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nbconvert/exporters/pdf.py b/nbconvert/exporters/pdf.py index 7ee7a50e5..7620866d7 100644 --- a/nbconvert/exporters/pdf.py +++ b/nbconvert/exporters/pdf.py @@ -66,7 +66,7 @@ class PDFExporter(LatexExporter): ).tag(config=True) texinputs = Unicode(help="texinputs dir. A notebook's directory is added") - writer = Instance("nbconvert.writers.FilesWriter", args=()) + writer = Instance("nbconvert.writers.FilesWriter", args=(), kw={'build_directory': '.'}) _captured_output = List() From 6bdba4f98b347924a01d0e1072b98378e3534540 Mon Sep 17 00:00:00 2001 From: Saul Shanabrook Date: Mon, 26 Feb 2018 14:19:37 -0500 Subject: [PATCH 113/671] Remove dead code for manual pdf cleanup --- nbconvert/exporters/pdf.py | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/nbconvert/exporters/pdf.py b/nbconvert/exporters/pdf.py index 7620866d7..d9998a9e8 100644 --- a/nbconvert/exporters/pdf.py +++ b/nbconvert/exporters/pdf.py @@ -61,10 +61,6 @@ class PDFExporter(LatexExporter): help="Whether to display the output of latex commands." ).tag(config=True) - temp_file_exts = List(['.aux', '.bbl', '.blg', '.idx', '.log', '.out'], - help="File extensions of temp files to remove after running." - ).tag(config=True) - texinputs = Unicode(help="texinputs dir. A notebook's directory is added") writer = Instance("nbconvert.writers.FilesWriter", args=(), kw={'build_directory': '.'}) @@ -155,16 +151,6 @@ def log_error(command, out): self.log.debug(u"%s output: %s\n%s", command[0], command, out) return self.run_command(self.bib_command, filename, 1, log_error) - - def clean_temp_files(self, filename): - """Remove temporary files created by xelatex/bibtex.""" - self.log.info("Removing temporary LaTeX files") - filename = os.path.splitext(filename)[0] - for ext in self.temp_file_exts: - try: - os.remove(filename+ext) - except OSError: - pass def from_notebook_node(self, nb, resources=None, **kw): latex, resources = super(PDFExporter, self).from_notebook_node( From 2210f10d96295906c330718c60b8189d8cded77a Mon Sep 17 00:00:00 2001 From: Saul Shanabrook Date: Tue, 27 Feb 2018 10:06:31 -0500 Subject: [PATCH 114/671] Verify all tmp files removed after pdf gen --- nbconvert/exporters/tests/test_pdf.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/nbconvert/exporters/tests/test_pdf.py b/nbconvert/exporters/tests/test_pdf.py index ca864eaf2..f9b0183d9 100644 --- a/nbconvert/exporters/tests/test_pdf.py +++ b/nbconvert/exporters/tests/test_pdf.py @@ -32,11 +32,11 @@ def test_constructor(self): def test_export(self): """Smoke test PDFExporter""" with tempdir.TemporaryDirectory() as td: - newpath = os.path.join(td, os.path.basename(self._get_notebook())) + file_name = os.path.basename(self._get_notebook()) + newpath = os.path.join(td, file_name) shutil.copy(self._get_notebook(), newpath) (output, resources) = self.exporter_class(latex_count=1).from_filename(newpath) self.assertIsInstance(output, bytes) assert len(output) > 0 - # tex file should be cleaned up - assert 'notebook.tex' not in os.listdir(td) - + # all temporary file should be cleaned up + assert {file_name} == set(os.listdir(td)) From 792f285ef49230818ae4fa0701d3bd79a0820c2b Mon Sep 17 00:00:00 2001 From: Lukasz Mitusinski Date: Fri, 16 Mar 2018 13:16:56 +0100 Subject: [PATCH 115/671] handling embeded images in html converter --- nbconvert/exporters/html.py | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/nbconvert/exporters/html.py b/nbconvert/exporters/html.py index 73e2a0952..9210e19fd 100644 --- a/nbconvert/exporters/html.py +++ b/nbconvert/exporters/html.py @@ -37,9 +37,9 @@ def _default_template_path_default(self): @default('template_file') def _template_file_default(self): return 'full.tpl' - + output_mimetype = 'text/html' - + @property def default_config(self): c = Config({ @@ -77,9 +77,21 @@ def default_filters(self): yield pair yield ('markdown2html', self.markdown2html) + def process_attachments(self, nb, output): + for cell in nb.cells: + if 'attachments' in cell: + for key, attachment in cell['attachments'].items(): + for att_type, att in attachment.items(): + output = output.replace( + 'attachment:{}'.format(key), + 'data:' + att_type + ';base64, ' + attachment[att_type]) + return output + def from_notebook_node(self, nb, resources=None, **kw): langinfo = nb.metadata.get('language_info', {}) lexer = langinfo.get('pygments_lexer', langinfo.get('name', None)) self.register_filter('highlight_code', Highlight2HTML(pygments_lexer=lexer, parent=self)) - return super(HTMLExporter, self).from_notebook_node(nb, resources, **kw) + output, resources = super(HTMLExporter, self).from_notebook_node(nb, resources, **kw) + att_output = self.process_attachments(nb, output) + return att_output, resources From 308fb2914a6528e44cffb9a697bac9d185e14fc3 Mon Sep 17 00:00:00 2001 From: Matthias Geier Date: Wed, 21 Mar 2018 17:55:19 +0100 Subject: [PATCH 116/671] Don't remove empty cells by default Fixes #720. --- nbconvert/preprocessors/regexremove.py | 14 ++++++-------- nbconvert/preprocessors/tests/test_regexremove.py | 4 ++-- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/nbconvert/preprocessors/regexremove.py b/nbconvert/preprocessors/regexremove.py index 7c185df52..91154cf28 100644 --- a/nbconvert/preprocessors/regexremove.py +++ b/nbconvert/preprocessors/regexremove.py @@ -20,16 +20,14 @@ class RegexRemovePreprocessor(Preprocessor): of unicode strings. If the contents match any of the patterns, the cell is removed from the notebook. - By default, `patterns = [r'\Z']` which matches the empty string such that - strictly empty cells are removed. To modify the list of matched patterns, + To modify the list of matched patterns, modify the patterns traitlet. For example, execute the following command - to convert a notebook to html and remove cells containing only whitespace: + to convert a notebook to html and remove cells containing only whitespace:: - > jupyter nbconvert --RegexRemovePreprocessor.enabled=True \ - --RegexRemovePreprocessor.patterns="['\\s*\\Z']" mynotebook.ipynb + jupyter nbconvert --RegexRemovePreprocessor.patterns="['\\s*\\Z']" mynotebook.ipynb - The first command line argument enables the preprocessor and the second - sets the list of patterns to '\\s*\\Z' which matches an arbitrary number + The command line argument + sets the list of patterns to ``'\\s*\\Z'`` which matches an arbitrary number of whitespace characters followed by the end of the string. See https://regex101.com/ for an interactive guide to regular expressions @@ -38,7 +36,7 @@ class RegexRemovePreprocessor(Preprocessor): documentation in python. """ - patterns = List(Unicode(), default_value=[r'\Z']).tag(config=True) + patterns = List(Unicode(), default_value=[]).tag(config=True) def check_conditions(self, cell): """ diff --git a/nbconvert/preprocessors/tests/test_regexremove.py b/nbconvert/preprocessors/tests/test_regexremove.py index 1024a7fca..3f7bc2903 100644 --- a/nbconvert/preprocessors/tests/test_regexremove.py +++ b/nbconvert/preprocessors/tests/test_regexremove.py @@ -44,9 +44,9 @@ def test_output(self): 'disallow_tab_newline': [r'\t\Z', r'\n\Z'] } expected_cell_count = { - 'default': 5, # only strictly empty cells + 'default': 6, # nothing is removed 'disallow_whitespace': 2, # all "empty" cells are removed - 'disallow_tab_newline': 3, # all "empty" cells but the single space + 'disallow_tab_newline': 4, # cells with tab and newline are removed 'none': 6, } for method in ['default', 'disallow_whitespace', 'disallow_tab_newline', 'none']: From cb50e63a620ccf24ac0edf25bec16024ea1c0354 Mon Sep 17 00:00:00 2001 From: Lukasz Mitusinski Date: Thu, 22 Mar 2018 12:39:01 +0100 Subject: [PATCH 117/671] test for attachments conversion --- .../exporters/tests/files/attachment.ipynb | 37 +++++++++++++++++++ nbconvert/exporters/tests/test_html.py | 9 +++++ 2 files changed, 46 insertions(+) create mode 100644 nbconvert/exporters/tests/files/attachment.ipynb diff --git a/nbconvert/exporters/tests/files/attachment.ipynb b/nbconvert/exporters/tests/files/attachment.ipynb new file mode 100644 index 000000000..1656bb776 --- /dev/null +++ b/nbconvert/exporters/tests/files/attachment.ipynb @@ -0,0 +1,37 @@ +{ + "cells": [ + { + "attachments": { + "python.png": { + "image/png": "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" + } + }, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "![python.png](attachment:python.png)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.6.4" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/nbconvert/exporters/tests/test_html.py b/nbconvert/exporters/tests/test_html.py index 1a771e023..cb8c88144 100644 --- a/nbconvert/exporters/tests/test_html.py +++ b/nbconvert/exporters/tests/test_html.py @@ -110,3 +110,12 @@ def test_javascript_output(self): ) (output, resources) = HTMLExporter(template_file='basic').from_notebook_node(nb) self.assertIn('javascript_output', output) + + def test_attachments(self): + (output, resources) = HTMLExporter(template_file='basic').from_file( + self._get_notebook(nb_name='attachment.ipynb') + ) + check_for_png = re.compile(r']*?)>') + result = check_for_png.search(output) + self.assertTrue(result.group(0).strip().startswith(' Date: Wed, 4 Apr 2018 13:19:07 +0200 Subject: [PATCH 118/671] Update widgets CDN for ipywidgets 7 w/fallback --- nbconvert/nbconvertapp.py | 5 +++++ nbconvert/templates/html/full.tpl | 33 +++++++++++++++++++++++++++---- 2 files changed, 34 insertions(+), 4 deletions(-) diff --git a/nbconvert/nbconvertapp.py b/nbconvert/nbconvertapp.py index 340ee7888..542fb3a16 100755 --- a/nbconvert/nbconvertapp.py +++ b/nbconvert/nbconvertapp.py @@ -246,6 +246,9 @@ def _postprocessor_class_changed(self, change): if new: self.postprocessor_factory = import_item(new) + ipywidgets_base_url = Unicode("https://unpkg.com/", + help="URL base for ipywidgets package").tag(config=True) + export_format = Unicode( 'html', @@ -361,6 +364,8 @@ def init_single_notebook_resources(self, notebook_filename): resources['output_files_dir'] = output_files_dir + resources['ipywidgets_base_url'] = self.ipywidgets_base_url + return resources def export_single_notebook(self, notebook_filename, resources, input_buffer=None): diff --git a/nbconvert/templates/html/full.tpl b/nbconvert/templates/html/full.tpl index 1443e1160..8cdffc664 100644 --- a/nbconvert/templates/html/full.tpl +++ b/nbconvert/templates/html/full.tpl @@ -11,13 +11,38 @@ {% set nb_title = nb.metadata.get('title', '') or resources['metadata']['name'] %} {{nb_title}} -{%- if "widgets" in nb.metadata -%} - -{%- endif-%} - +{% block ipywidgets %} +{%- if "widgets" in nb.metadata -%} + +{%- endif -%} +{% endblock ipywidgets %} + {% for css in resources.inlining.css -%} \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "\n" + ], + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "%matplotlib inline\n", + "%config InlineBackend.figure_formats = ['svg'] \n", + "import matplotlib.pyplot as plt\n", + "plt.plot((0,1,2,3,4,5),(0,3,4,4,3,0))" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.6.3" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/nbconvert/exporters/tests/test_latex.py b/nbconvert/exporters/tests/test_latex.py index fa92a6c41..6f3b9a9b2 100644 --- a/nbconvert/exporters/tests/test_latex.py +++ b/nbconvert/exporters/tests/test_latex.py @@ -18,6 +18,10 @@ from jinja2 import DictLoader + +current_dir = os.path.dirname(__file__) + + class TestLatexExporter(ExportersTestsBase): """Contains test functions for latex.py""" @@ -134,13 +138,22 @@ def test_no_prompt_yes_input(self): self._get_notebook(nb_name="prompt_numbers.ipynb")) assert "shape" in output assert "evs" in output - + + @onlyif_cmds_exist('pandoc') + def test_svg(self): + """ + Can a LatexExporter export when it recieves raw binary strings form svg? + """ + filename = os.path.join(current_dir, 'files', 'svg.ipynb') + (output, resources) = LatexExporter().from_filename(filename) + assert len(output) > 0 + def test_in_memory_template_tplx(self): # Loads in an in memory latex template (.tplx) using jinja2.DictLoader # creates a class that uses this template with the template_file argument # converts an empty notebook using this mechanism my_loader_tplx = DictLoader({'my_template': "{%- extends 'article.tplx' -%}"}) - + class MyExporter(LatexExporter): template_file = 'my_template' diff --git a/nbconvert/preprocessors/extractoutput.py b/nbconvert/preprocessors/extractoutput.py index 3584c09a1..6b59b4828 100755 --- a/nbconvert/preprocessors/extractoutput.py +++ b/nbconvert/preprocessors/extractoutput.py @@ -34,7 +34,7 @@ def guess_extension_without_jpe(mimetype): class ExtractOutputPreprocessor(Preprocessor): """ - Extracts all of the outputs from the notebook file. The extracted + Extracts all of the outputs from the notebook file. The extracted outputs are returned in the 'resources' dictionary. """ @@ -49,7 +49,7 @@ class ExtractOutputPreprocessor(Preprocessor): def preprocess_cell(self, cell, resources, cell_index): """ Apply a transformation on each cell, - + Parameters ---------- cell : NotebookNode cell @@ -61,16 +61,16 @@ def preprocess_cell(self, cell, resources, cell_index): Index of the cell being processed (see base.py) """ - #Get the unique key from the resource dict if it exists. If it does not + #Get the unique key from the resource dict if it exists. If it does not #exist, use 'output' as the default. Also, get files directory if it #has been specified unique_key = resources.get('unique_key', 'output') output_files_dir = resources.get('output_files_dir', None) - + #Make sure outputs key exists if not isinstance(resources['outputs'], dict): resources['outputs'] = {} - + #Loop through all of the outputs in the cell for index, out in enumerate(cell.get('outputs', [])): if out.output_type not in {'display_data', 'execute_result'}: @@ -89,6 +89,11 @@ def preprocess_cell(self, cell, resources, cell_index): # JSON. In the latter case we want to go extra sure that # we enclose a scalar string value into extra quotes by # serializing it properly. + if isinstance(data, bytes): + # In python 3 we need to guess the encoding in this + # instance. Some modules that return raw data like + # svg can leave the data in byte form instead of str + data = data.decode('utf-8') data = json.dumps(data) #Binary files are base64-encoded, SVG is already XML @@ -100,7 +105,7 @@ def preprocess_cell(self, cell, resources, cell_index): data = data.replace('\n', '\r\n').encode("UTF-8") else: data = data.encode("UTF-8") - + ext = guess_extension_without_jpe(mime_type) if ext is None: ext = '.' + mime_type.rsplit('/')[-1] From 7cc12431de2bc8a77f13931914c454087f5fb9f6 Mon Sep 17 00:00:00 2001 From: Tyler Makaro Date: Wed, 10 Apr 2019 19:41:26 -0700 Subject: [PATCH 246/671] Make latex errors less verbose --- nbconvert/exporters/pdf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nbconvert/exporters/pdf.py b/nbconvert/exporters/pdf.py index 4f6708d1d..20fd0d973 100644 --- a/nbconvert/exporters/pdf.py +++ b/nbconvert/exporters/pdf.py @@ -50,7 +50,7 @@ class PDFExporter(LatexExporter): help="How many times latex will be called." ).tag(config=True) - latex_command = List([u"xelatex", u"{filename}"], + latex_command = List([u"xelatex", u"{filename}", "-quiet"], help="Shell command used to compile latex." ).tag(config=True) From d81be9c4d4b990759bca096f273bb34c607ec19c Mon Sep 17 00:00:00 2001 From: Matthew Seal Date: Sat, 13 Apr 2019 14:28:14 -0700 Subject: [PATCH 247/671] Fixed binary data conversion to not try converting non-json data --- nbconvert/preprocessors/extractoutput.py | 20 ++++++++----------- .../preprocessors/tests/test_extractoutput.py | 4 ++-- 2 files changed, 10 insertions(+), 14 deletions(-) diff --git a/nbconvert/preprocessors/extractoutput.py b/nbconvert/preprocessors/extractoutput.py index 6b59b4828..80fc54ff0 100755 --- a/nbconvert/preprocessors/extractoutput.py +++ b/nbconvert/preprocessors/extractoutput.py @@ -80,31 +80,27 @@ def preprocess_cell(self, cell, resources, cell_index): if mime_type in out.data: data = out.data[mime_type] - if ( - not isinstance(data, text_type) - or mime_type == 'application/json' - ): + if mime_type == 'application/json': # Data is either JSON-like and was parsed into a Python # object according to the spec, or data is for sure # JSON. In the latter case we want to go extra sure that # we enclose a scalar string value into extra quotes by # serializing it properly. - if isinstance(data, bytes): + if isinstance(data, bytes) and not isinstance(data, text_type): # In python 3 we need to guess the encoding in this # instance. Some modules that return raw data like # svg can leave the data in byte form instead of str data = data.decode('utf-8') data = json.dumps(data) - - #Binary files are base64-encoded, SVG is already XML - if mime_type in {'image/png', 'image/jpeg', 'application/pdf'}: + # Binary files are base64-encoded, SVG is already XML + elif mime_type in {'image/png', 'image/jpeg', 'application/pdf'}: # data is b64-encoded as text (str, unicode), # we want the original bytes data = a2b_base64(data) - elif sys.platform == 'win32': - data = data.replace('\n', '\r\n').encode("UTF-8") - else: - data = data.encode("UTF-8") + elif isinstance(data, text_type): + if sys.platform == 'win32': + data = data.replace('\n', '\r\n') + data = data.encode('utf-8') ext = guess_extension_without_jpe(mime_type) if ext is None: diff --git a/nbconvert/preprocessors/tests/test_extractoutput.py b/nbconvert/preprocessors/tests/test_extractoutput.py index 8d83d7118..2c3f84a5a 100644 --- a/nbconvert/preprocessors/tests/test_extractoutput.py +++ b/nbconvert/preprocessors/tests/test_extractoutput.py @@ -40,7 +40,7 @@ def test_output(self): self.assertIn('filenames', output.metadata) self.assertIn('image/png', output.metadata.filenames) png_filename = output.metadata.filenames['image/png'] - + # Check that pdf was extracted output = nb.cells[0].outputs[7] self.assertIn('filenames', output.metadata) @@ -78,7 +78,7 @@ def test_json_extraction(self): for out in reference: try: data = out['data']['application/json'] - reference_files.append(json.dumps(data).encode()) + reference_files.append(json.dumps(data)) except KeyError: pass From 624892009d00226c4f165b490adbc1c4a035605d Mon Sep 17 00:00:00 2001 From: Matthew Seal Date: Sun, 14 Apr 2019 12:52:31 -0700 Subject: [PATCH 248/671] Applied PR feedback to be less strict in extractoutput --- nbconvert/preprocessors/extractoutput.py | 28 +++++++++++-------- .../preprocessors/tests/test_extractoutput.py | 2 +- 2 files changed, 18 insertions(+), 12 deletions(-) diff --git a/nbconvert/preprocessors/extractoutput.py b/nbconvert/preprocessors/extractoutput.py index 80fc54ff0..63280b7ee 100755 --- a/nbconvert/preprocessors/extractoutput.py +++ b/nbconvert/preprocessors/extractoutput.py @@ -32,6 +32,13 @@ def guess_extension_without_jpe(mimetype): ext=".jpeg" return ext +def platform_utf_8_encode(data): + if isinstance(data, text_type): + if sys.platform == 'win32': + data = data.replace('\n', '\r\n') + data = data.encode('utf-8') + return data + class ExtractOutputPreprocessor(Preprocessor): """ Extracts all of the outputs from the notebook file. The extracted @@ -80,7 +87,12 @@ def preprocess_cell(self, cell, resources, cell_index): if mime_type in out.data: data = out.data[mime_type] - if mime_type == 'application/json': + # Binary files are base64-encoded, SVG is already XML + if mime_type in {'image/png', 'image/jpeg', 'application/pdf'}: + # data is b64-encoded as text (str, unicode), + # we want the original bytes + data = a2b_base64(data) + elif mime_type == 'application/json' or not isinstance(data, text_type): # Data is either JSON-like and was parsed into a Python # object according to the spec, or data is for sure # JSON. In the latter case we want to go extra sure that @@ -91,16 +103,10 @@ def preprocess_cell(self, cell, resources, cell_index): # instance. Some modules that return raw data like # svg can leave the data in byte form instead of str data = data.decode('utf-8') - data = json.dumps(data) - # Binary files are base64-encoded, SVG is already XML - elif mime_type in {'image/png', 'image/jpeg', 'application/pdf'}: - # data is b64-encoded as text (str, unicode), - # we want the original bytes - data = a2b_base64(data) - elif isinstance(data, text_type): - if sys.platform == 'win32': - data = data.replace('\n', '\r\n') - data = data.encode('utf-8') + data = platform_utf_8_encode(json.dumps(data)) + else: + # All other text_type data will fall into this path + data = platform_utf_8_encode(data) ext = guess_extension_without_jpe(mime_type) if ext is None: diff --git a/nbconvert/preprocessors/tests/test_extractoutput.py b/nbconvert/preprocessors/tests/test_extractoutput.py index 2c3f84a5a..a6db58d07 100644 --- a/nbconvert/preprocessors/tests/test_extractoutput.py +++ b/nbconvert/preprocessors/tests/test_extractoutput.py @@ -78,7 +78,7 @@ def test_json_extraction(self): for out in reference: try: data = out['data']['application/json'] - reference_files.append(json.dumps(data)) + reference_files.append(json.dumps(data).encode()) except KeyError: pass From e9bb1dc37e9fe775690fa8bb1ad224f1af4d2eb0 Mon Sep 17 00:00:00 2001 From: Philipp A Date: Mon, 15 Apr 2019 12:09:38 +0200 Subject: [PATCH 249/671] Mention formats in --to documentation --- nbconvert/nbconvertapp.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/nbconvert/nbconvertapp.py b/nbconvert/nbconvertapp.py index 032c1b6f6..f70e99a6d 100755 --- a/nbconvert/nbconvertapp.py +++ b/nbconvert/nbconvertapp.py @@ -181,7 +181,7 @@ def _classes_default(self): which will convert mynotebook.ipynb to the default format (probably HTML). You can specify the export format with `--to`. - Options include {0} + Options include {formats}. > jupyter nbconvert --to latex mynotebook.ipynb @@ -214,7 +214,7 @@ def _classes_default(self): c.NbConvertApp.notebooks = ["my_notebook.ipynb"] > jupyter nbconvert --config mycfg.py - """.format(get_export_names())) + """.format(formats=get_export_names())) # Writer specific variables writer = Instance('nbconvert.writers.base.WriterBase', @@ -262,9 +262,10 @@ def _postprocessor_class_changed(self, change): export_format = Unicode( 'html', allow_none=False, - help="""The export format to be used, either one of the built-in formats, + help="""The export format to be used, either one of the built-in formats + {formats} or a dotted object name that represents the import path for an - `Exporter` class""" + `Exporter` class""".format(formats=get_export_names()) ).tag(config=True) notebooks = List([], help="""List of notebooks to convert. From 647060732bf6b60227f47c5f6e96ccf925e49ad0 Mon Sep 17 00:00:00 2001 From: Matthew Seal Date: Sat, 6 Apr 2019 21:35:37 -0700 Subject: [PATCH 250/671] Added tests for each branch in execute's run_cell method --- nbconvert/preprocessors/tests/test_execute.py | 492 +++++++++++++++++- 1 file changed, 474 insertions(+), 18 deletions(-) diff --git a/nbconvert/preprocessors/tests/test_execute.py b/nbconvert/preprocessors/tests/test_execute.py index 724673c2a..9c5b8c119 100644 --- a/nbconvert/preprocessors/tests/test_execute.py +++ b/nbconvert/preprocessors/tests/test_execute.py @@ -17,12 +17,13 @@ import nbformat import sys import pytest +import functools from .base import PreprocessorTestsBase from ..execute import ExecutePreprocessor, CellExecutionError, executenb import IPython -from mock import patch +from mock import patch, MagicMock from traitlets import TraitError from jupyter_client.kernelspec import KernelSpecManager from nbconvert.filters import strip_ansi @@ -46,7 +47,64 @@ def _normalize_base64(b64_text): except (ValueError, TypeError): return b64_text -class TestExecute(PreprocessorTestsBase): + +def merge_dicts(first, second): + # Because this is annoying to do inline + outcome = {} + outcome.update(first) + outcome.update(second) + return outcome + + +class ExecuteTestBase(PreprocessorTestsBase): + def build_preprocessor(self, opts): + """Make an instance of a preprocessor""" + preprocessor = ExecutePreprocessor() + preprocessor.enabled = True + for opt in opts: + setattr(preprocessor, opt, opts[opt]) + # Perform some state setup that should probably be in the init + preprocessor._display_id_map = {} + preprocessor.widget_state = {} + preprocessor.widget_buffers = {} + return preprocessor + + @staticmethod + def prepare_cell_mocks(*messages): + messages = list(messages) + def prepared_wrapper(func): + @functools.wraps(func) + def test_mock_wrapper(self): + parent_id = 'fake_id' + cell_mock = MagicMock(source='"foo" = "bar"', outputs=[]) + # Hack to help catch `.` and `[]` style access to outputs against the same mock object + cell_mock.__getitem__.side_effect = lambda n: cell_mock.outputs if n == 'outputs' else None + preprocessor = self.build_preprocessor({}) + preprocessor.nb = {'cells': [cell_mock]} + shell_message_mock = MagicMock( + return_value={'parent_header': {'msg_id': parent_id}}) + # Always terminate messages with an idle to exit the loop + messages.append({'msg_type': 'status', 'content': {'execution_state': 'idle'}}) + message_mock = MagicMock( + side_effect=[ + # Default the parent_header so mocks don't need to include this + merge_dicts({'parent_header': {'msg_id': parent_id}}, msg) + for msg in messages + ] + ) + channel_mock = MagicMock(get_msg=message_mock) + shell_mock = MagicMock(get_msg=shell_message_mock) + preprocessor.kc = MagicMock( + iopub_channel=channel_mock, + shell_channel=shell_mock, + execute=MagicMock(return_value=parent_id) + ) + return func(self, preprocessor, cell_mock, message_mock) + return test_mock_wrapper + return prepared_wrapper + + +class TestExecute(ExecuteTestBase): """Contains test functions for execute.py""" maxDiff = None @@ -98,15 +156,6 @@ def assert_notebooks_equal(self, expected, actual): self.assertEqual(expected_execution_count, actual_execution_count) - def build_preprocessor(self, opts): - """Make an instance of a preprocessor""" - preprocessor = ExecutePreprocessor() - preprocessor.enabled = True - for opt in opts: - setattr(preprocessor, opt, opts[opt]) - return preprocessor - - def test_constructor(self): """Can a ExecutePreprocessor be constructed?""" self.build_preprocessor({}) @@ -212,7 +261,6 @@ def test_disable_stdin(self): def test_timeout(self): """Check that an error is raised when a computation times out""" - current_dir = os.path.dirname(__file__) filename = os.path.join(current_dir, 'files', 'Interrupt.ipynb') res = self.build_resources() res['metadata']['path'] = os.path.dirname(filename) @@ -222,7 +270,6 @@ def test_timeout(self): def test_timeout_func(self): """Check that an error is raised when a computation times out""" - current_dir = os.path.dirname(__file__) filename = os.path.join(current_dir, 'files', 'Interrupt.ipynb') res = self.build_resources() res['metadata']['path'] = os.path.dirname(filename) @@ -249,7 +296,6 @@ def test_allow_errors(self): """ Check that conversion halts if ``allow_errors`` is False. """ - current_dir = os.path.dirname(__file__) filename = os.path.join(current_dir, 'files', 'Skip Exceptions.ipynb') res = self.build_resources() res['metadata']['path'] = os.path.dirname(filename) @@ -266,7 +312,6 @@ def test_force_raise_errors(self): Check that conversion halts if the ``force_raise_errors`` traitlet on ExecutePreprocessor is set to True. """ - current_dir = os.path.dirname(__file__) filename = os.path.join(current_dir, 'files', 'Skip Exceptions with Cell Tags.ipynb') res = self.build_resources() @@ -282,8 +327,6 @@ def test_force_raise_errors(self): def test_custom_kernel_manager(self): from .fake_kernelmanager import FakeCustomKernelManager - current_dir = os.path.dirname(__file__) - filename = os.path.join(current_dir, 'files', 'HelloWorld.ipynb') with io.open(filename) as f: @@ -311,7 +354,6 @@ def test_custom_kernel_manager(self): def test_execute_function(self): # Test the executenb() convenience API - current_dir = os.path.dirname(__file__) filename = os.path.join(current_dir, 'files', 'HelloWorld.ipynb') with io.open(filename) as f: @@ -350,3 +392,417 @@ def test_widgets(self): assert 'state' in d assert 'version_major' in wdata assert 'version_minor' in wdata + + +class TestRunCell(ExecuteTestBase): + """Contains test functions for ExecutePreprocessor.run_cell""" + + @ExecuteTestBase.prepare_cell_mocks() + def test_idle_message(self, preprocessor, cell_mock, message_mock): + preprocessor.run_cell(cell_mock) + # Just the exit message should be fetched + message_mock.assert_called_once() + # Ensure no outputs were generated + self.assertFalse(cell_mock.outputs) + + @ExecuteTestBase.prepare_cell_mocks({ + 'msg_type': 'stream', + 'header': {'msg_type': 'execute_reply'}, + 'parent_header': {'msg_id': 'wrong_parent'}, + 'content': {'name': 'stdout', 'text': 'foo'} + }) + def test_message_for_wrong_parent(self, preprocessor, cell_mock, message_mock): + preprocessor.run_cell(cell_mock) + # An ignored stream followed by an idle + self.assertEqual(message_mock.call_count, 2) + # Ensure no output was written + self.assertFalse(cell_mock.outputs) + + @ExecuteTestBase.prepare_cell_mocks({ + 'msg_type': 'status', + 'header': {'msg_type': 'status'}, + 'content': {'execution_state': 'busy'} + }) + def test_busy_message(self, preprocessor, cell_mock, message_mock): + preprocessor.run_cell(cell_mock) + # One busy message, followed by an idle + self.assertEqual(message_mock.call_count, 2) + # Ensure no outputs were generated + self.assertFalse(cell_mock.outputs) + + @ExecuteTestBase.prepare_cell_mocks({ + 'msg_type': 'execute_input', + 'header': {'msg_type': 'execute_input'}, + 'content': {} + }) + def test_execute_input_message(self, preprocessor, cell_mock, message_mock): + preprocessor.run_cell(cell_mock) + # One ignored execute_input, followed by an idle + self.assertEqual(message_mock.call_count, 2) + # Ensure no outputs were generated + self.assertFalse(cell_mock.outputs) + + @ExecuteTestBase.prepare_cell_mocks({ + 'msg_type': 'stream', + 'header': {'msg_type': 'stream'}, + 'content': {'name': 'stdout', 'text': 'foo'}, + }, { + 'msg_type': 'stream', + 'header': {'msg_type': 'stream'}, + 'content': {'name': 'stderr', 'text': 'bar'} + }) + def test_stream_messages(self, preprocessor, cell_mock, message_mock): + preprocessor.run_cell(cell_mock) + # An stdout then stderr stream followed by an idle + self.assertEqual(message_mock.call_count, 3) + # Ensure the output was captured + self.assertListEqual(cell_mock.outputs, [ + {'output_type': 'stream', 'name': 'stdout', 'text': 'foo'}, + {'output_type': 'stream', 'name': 'stderr', 'text': 'bar'} + ]) + + @ExecuteTestBase.prepare_cell_mocks({ + 'msg_type': 'stream', + 'header': {'msg_type': 'execute_reply'}, + 'content': {'name': 'stdout', 'text': 'foo'} + }, { + 'msg_type': 'clear_output', + 'header': {'msg_type': 'clear_output'}, + 'content': {} + }) + def test_clear_output_message(self, preprocessor, cell_mock, message_mock): + preprocessor.run_cell(cell_mock) + # A stream, followed by a clear, and then an idle + self.assertEqual(message_mock.call_count, 3) + # Ensure the output was cleared + self.assertFalse(cell_mock.outputs) + + @ExecuteTestBase.prepare_cell_mocks({ + 'msg_type': 'stream', + 'header': {'msg_type': 'stream'}, + 'content': {'name': 'stdout', 'text': 'foo'} + }, { + 'msg_type': 'clear_output', + 'header': {'msg_type': 'clear_output'}, + 'content': {'wait': True} + }) + def test_clear_output_wait_message(self, preprocessor, cell_mock, message_mock): + preprocessor.run_cell(cell_mock) + # A stream, followed by a clear, and then an idle + self.assertEqual(message_mock.call_count, 3) + # Should be true without another message to trigger the clear + self.assertTrue(preprocessor.clear_before_next_output) + # Ensure the output wasn't cleared yet + self.assertListEqual(cell_mock.outputs, [ + {'output_type': 'stream', 'name': 'stdout', 'text': 'foo'} + ]) + + @ExecuteTestBase.prepare_cell_mocks({ + 'msg_type': 'stream', + 'header': {'msg_type': 'stream'}, + 'content': {'name': 'stdout', 'text': 'foo'} + }, { + 'msg_type': 'clear_output', + 'header': {'msg_type': 'clear_output'}, + 'content': {'wait': True} + }, { + 'msg_type': 'stream', + 'header': {'msg_type': 'stream'}, + 'content': {'name': 'stderr', 'text': 'bar'} + }) + def test_clear_output_wait_then_message_message(self, preprocessor, cell_mock, message_mock): + preprocessor.run_cell(cell_mock) + # An stdout stream, followed by a wait clear, an stderr stream, and then an idle + self.assertEqual(message_mock.call_count, 4) + # Should be false after the stderr message + self.assertFalse(preprocessor.clear_before_next_output) + # Ensure the output wasn't cleared yet + self.assertListEqual(cell_mock.outputs, [ + {'output_type': 'stream', 'name': 'stderr', 'text': 'bar'} + ]) + + @ExecuteTestBase.prepare_cell_mocks({ + 'msg_type': 'execute_reply', + 'header': {'msg_type': 'execute_reply'}, + 'content': {'execution_count': 42} + }) + def test_execution_count_message(self, preprocessor, cell_mock, message_mock): + preprocessor.run_cell(cell_mock) + # An execution count followed by an idle + self.assertEqual(message_mock.call_count, 2) + cell_mock.__setitem__.assert_called_once_with('execution_count', 42) + # Ensure no outputs were generated + self.assertFalse(cell_mock.outputs) + + @ExecuteTestBase.prepare_cell_mocks({ + 'msg_type': 'stream', + 'header': {'msg_type': 'stream'}, + 'content': {'execution_count': 42, 'name': 'stdout', 'text': 'foo'} + }) + def test_execution_count_with_stream_message(self, preprocessor, cell_mock, message_mock): + preprocessor.run_cell(cell_mock) + # An execution count followed by an idle + self.assertEqual(message_mock.call_count, 2) + cell_mock.__setitem__.assert_called_once_with('execution_count', 42) + # Should also consume the message stream + self.assertListEqual(cell_mock.outputs, [ + {'output_type': 'stream', 'name': 'stdout', 'text': 'foo'} + ]) + + @ExecuteTestBase.prepare_cell_mocks({ + 'msg_type': 'comm', + 'header': {'msg_type': 'comm'}, + 'content': { + 'comm_id': 'foobar', + 'data': {'state': {'foo': 'bar'}} + } + }) + def test_widget_comm_message(self, preprocessor, cell_mock, message_mock): + preprocessor.run_cell(cell_mock) + # A comm message without buffer info followed by an idle + self.assertEqual(message_mock.call_count, 2) + self.assertEqual(preprocessor.widget_state, {'foobar': {'foo': 'bar'}}) + # Buffers should still be empty + self.assertFalse(preprocessor.widget_buffers) + # Ensure no outputs were generated + self.assertFalse(cell_mock.outputs) + + @ExecuteTestBase.prepare_cell_mocks({ + 'msg_type': 'comm', + 'header': {'msg_type': 'comm'}, + 'buffers': [b'123'], + 'content': { + 'comm_id': 'foobar', + 'data': { + 'state': {'foo': 'bar'}, + 'buffer_paths': ['path'] + } + } + }) + def test_widget_comm_buffer_message(self, preprocessor, cell_mock, message_mock): + preprocessor.run_cell(cell_mock) + # A comm message with buffer info followed by an idle + self.assertEqual(message_mock.call_count, 2) + self.assertEqual(preprocessor.widget_state, {'foobar': {'foo': 'bar'}}) + self.assertEqual(preprocessor.widget_buffers, + {'foobar': [{'data': 'MTIz', 'encoding': 'base64', 'path': 'path'}]} + ) + # Ensure no outputs were generated + self.assertFalse(cell_mock.outputs) + + @ExecuteTestBase.prepare_cell_mocks({ + 'msg_type': 'comm', + 'header': {'msg_type': 'comm'}, + 'content': { + 'comm_id': 'foobar', + # No 'state' + 'data': {'foo': 'bar'} + } + }) + def test_unknown_comm_message(self, preprocessor, cell_mock, message_mock): + preprocessor.run_cell(cell_mock) + # An unknown comm message followed by an idle + self.assertEqual(message_mock.call_count, 2) + # Widget states should be empty as the message has the wrong shape + self.assertFalse(preprocessor.widget_state) + self.assertFalse(preprocessor.widget_buffers) + # Ensure no outputs were generated + self.assertFalse(cell_mock.outputs) + + @ExecuteTestBase.prepare_cell_mocks({ + 'msg_type': 'execute_result', + 'header': {'msg_type': 'execute_result'}, + 'content': { + 'metadata': {'metafoo': 'metabar'}, + 'data': {'foo': 'bar'}, + 'execution_count': 42 + } + }) + def test_execute_result_message(self, preprocessor, cell_mock, message_mock): + preprocessor.run_cell(cell_mock) + # An execute followed by an idle + self.assertEqual(message_mock.call_count, 2) + cell_mock.__setitem__.assert_called_once_with('execution_count', 42) + # Should generate an associated message + self.assertListEqual(cell_mock.outputs, [{ + 'output_type': 'execute_result', + 'metadata': {'metafoo': 'metabar'}, + 'data': {'foo': 'bar'}, + 'execution_count': 42 + }]) + # No display id was provided + self.assertFalse(preprocessor._display_id_map) + + @ExecuteTestBase.prepare_cell_mocks({ + 'msg_type': 'execute_result', + 'header': {'msg_type': 'execute_result'}, + 'content': { + 'transient': {'display_id': 'foobar'}, + 'metadata': {'metafoo': 'metabar'}, + 'data': {'foo': 'bar'}, + 'execution_count': 42 + } + }) + def test_execute_result_with_display_message(self, preprocessor, cell_mock, message_mock): + preprocessor.run_cell(cell_mock) + # An execute followed by an idle + self.assertEqual(message_mock.call_count, 2) + cell_mock.__setitem__.assert_called_once_with('execution_count', 42) + # Should generate an associated message + self.assertListEqual(cell_mock.outputs, [{ + 'output_type': 'execute_result', + 'metadata': {'metafoo': 'metabar'}, + 'data': {'foo': 'bar'}, + 'execution_count': 42 + }]) + self.assertTrue('foobar' in preprocessor._display_id_map) + + @ExecuteTestBase.prepare_cell_mocks({ + 'msg_type': 'display_data', + 'header': {'msg_type': 'display_data'}, + 'content': {'metadata': {'metafoo': 'metabar'}, 'data': {'foo': 'bar'}} + }) + def test_display_data_without_id_message(self, preprocessor, cell_mock, message_mock): + preprocessor.run_cell(cell_mock) + # A display followed by an idle + self.assertEqual(message_mock.call_count, 2) + # Should generate an associated message + self.assertListEqual(cell_mock.outputs, [{ + 'output_type': 'display_data', + 'metadata': {'metafoo': 'metabar'}, + 'data': {'foo': 'bar'} + }]) + # No display id was provided + self.assertFalse(preprocessor._display_id_map) + + @ExecuteTestBase.prepare_cell_mocks({ + 'msg_type': 'display_data', + 'header': {'msg_type': 'display_data'}, + 'content': { + 'transient': {'display_id': 'foobar'}, + 'metadata': {'metafoo': 'metabar'}, + 'data': {'foo': 'bar'} + } + }) + def test_display_data_message(self, preprocessor, cell_mock, message_mock): + preprocessor.run_cell(cell_mock) + # A display followed by an idle + self.assertEqual(message_mock.call_count, 2) + # Should generate an associated message + self.assertListEqual(cell_mock.outputs, [{ + 'output_type': 'display_data', + 'metadata': {'metafoo': 'metabar'}, + 'data': {'foo': 'bar'} + }]) + self.assertTrue('foobar' in preprocessor._display_id_map) + + @ExecuteTestBase.prepare_cell_mocks({ + 'msg_type': 'display_data', + 'header': {'msg_type': 'display_data'}, + 'content': { + 'transient': {'display_id': 'foobar'}, + 'metadata': {'metafoo': 'metabar'}, + 'data': {'foo': 'bar'} + } + }, { + 'msg_type': 'display_data', + 'header': {'msg_type': 'display_data'}, + 'content': { + 'transient': {'display_id': 'foobar'}, + 'metadata': {'metafoo2': 'metabar2'}, + 'data': {'foo': 'bar2', 'baz': 'foobarbaz'} + } + }) + def test_display_data_same_id_message(self, preprocessor, cell_mock, message_mock): + preprocessor.run_cell(cell_mock) + # A display followed by an idle + self.assertEqual(message_mock.call_count, 3) + # Original output should be manipulated and a copy of the second now + self.assertListEqual(cell_mock.outputs, [{ + 'output_type': 'display_data', + 'metadata': {'metafoo2': 'metabar2'}, + 'data': {'foo': 'bar2', 'baz': 'foobarbaz'} + }, { + 'output_type': 'display_data', + 'metadata': {'metafoo2': 'metabar2'}, + 'data': {'foo': 'bar2', 'baz': 'foobarbaz'} + }]) + self.assertTrue('foobar' in preprocessor._display_id_map) + + @ExecuteTestBase.prepare_cell_mocks({ + 'msg_type': 'update_display_data', + 'header': {'msg_type': 'update_display_data'}, + 'content': {'metadata': {'metafoo': 'metabar'}, 'data': {'foo': 'bar'}} + }) + def test_update_display_data_without_id_message(self, preprocessor, cell_mock, message_mock): + preprocessor.run_cell(cell_mock) + # An update followed by an idle + self.assertEqual(message_mock.call_count, 2) + # Display updates don't create any outputs + self.assertFalse(cell_mock.outputs) + # No display id was provided + self.assertFalse(preprocessor._display_id_map) + + @ExecuteTestBase.prepare_cell_mocks({ + 'msg_type': 'update_display_data', + 'header': {'msg_type': 'update_display_data'}, + 'content': { + 'transient': {'display_id': 'foobar'}, + 'metadata': {'metafoo2': 'metabar2'}, + 'data': {'foo': 'bar2', 'baz': 'foobarbaz'} + } + }) + def test_update_display_data_mismatch_id_message(self, preprocessor, cell_mock, message_mock): + preprocessor.run_cell(cell_mock) + # An update followed by an idle + self.assertEqual(message_mock.call_count, 2) + # Display updates don't create any outputs + self.assertFalse(cell_mock.outputs) + # Display id wasn't found, so message was skipped + self.assertFalse(preprocessor._display_id_map) + + @ExecuteTestBase.prepare_cell_mocks({ + 'msg_type': 'display_data', + 'header': {'msg_type': 'display_data'}, + 'content': { + 'transient': {'display_id': 'foobar'}, + 'metadata': {'metafoo': 'metabar'}, + 'data': {'foo': 'bar'} + } + }, { + 'msg_type': 'update_display_data', + 'header': {'msg_type': 'update_display_data'}, + 'content': { + 'transient': {'display_id': 'foobar'}, + 'metadata': {'metafoo2': 'metabar2'}, + 'data': {'foo': 'bar2', 'baz': 'foobarbaz'} + } + }) + def test_update_display_data_message(self, preprocessor, cell_mock, message_mock): + preprocessor.run_cell(cell_mock) + # A display followed by an update then an idle + self.assertEqual(message_mock.call_count, 3) + # Original output should be manipulated + self.assertListEqual(cell_mock.outputs, [{ + 'output_type': 'display_data', + 'metadata': {'metafoo2': 'metabar2'}, + 'data': {'foo': 'bar2', 'baz': 'foobarbaz'} + }]) + self.assertTrue('foobar' in preprocessor._display_id_map) + + @ExecuteTestBase.prepare_cell_mocks({ + 'msg_type': 'error', + 'header': {'msg_type': 'error'}, + 'content': {'ename': 'foo', 'evalue': 'bar', 'traceback': ['Boom']} + }) + def test_error_message(self, preprocessor, cell_mock, message_mock): + preprocessor.run_cell(cell_mock) + # An error followed by an idle + self.assertEqual(message_mock.call_count, 2) + # Should also consume the message stream + self.assertListEqual(cell_mock.outputs, [{ + 'output_type': 'error', + 'ename': 'foo', + 'evalue': 'bar', + 'traceback': ['Boom'] + }]) From 1b784e8519c1fa27409e11e329926046a500430d Mon Sep 17 00:00:00 2001 From: Matthew Seal Date: Mon, 15 Apr 2019 17:51:39 -0700 Subject: [PATCH 251/671] Applied test assert changes and added additional tests based on feedback --- nbconvert/preprocessors/execute.py | 7 +- nbconvert/preprocessors/tests/test_execute.py | 208 +++++++++++------- 2 files changed, 134 insertions(+), 81 deletions(-) diff --git a/nbconvert/preprocessors/execute.py b/nbconvert/preprocessors/execute.py index 4cded0603..01c8a8120 100644 --- a/nbconvert/preprocessors/execute.py +++ b/nbconvert/preprocessors/execute.py @@ -463,7 +463,6 @@ def _wait_for_reply(self, msg_id, cell=None): self.log.error( "Kernel died while waiting for execute reply.") raise RuntimeError("Kernel died") - # kernel still alive, wait for a message continue # message received @@ -485,9 +484,9 @@ def _wait_for_reply(self, msg_id, cell=None): continue def run_cell(self, cell, cell_index=0): - msg_id = self.kc.execute(cell.source) + parent_msg_id = self.kc.execute(cell.source) self.log.debug("Executing cell:\n%s", cell.source) - exec_reply = self._wait_for_reply(msg_id, cell) + exec_reply = self._wait_for_reply(parent_msg_id, cell) outs = cell.outputs = [] self.clear_before_next_output = False @@ -506,7 +505,7 @@ def run_cell(self, cell, cell_index=0): raise RuntimeError("Timeout waiting for IOPub output") else: break - if msg['parent_header'].get('msg_id') != msg_id: + if msg['parent_header'].get('msg_id') != parent_msg_id: # not an output from our execution continue diff --git a/nbconvert/preprocessors/tests/test_execute.py b/nbconvert/preprocessors/tests/test_execute.py index 9c5b8c119..1af5730bf 100644 --- a/nbconvert/preprocessors/tests/test_execute.py +++ b/nbconvert/preprocessors/tests/test_execute.py @@ -25,6 +25,7 @@ import IPython from mock import patch, MagicMock from traitlets import TraitError +from nbformat import NotebookNode from jupyter_client.kernelspec import KernelSpecManager from nbconvert.filters import strip_ansi from testpath import modified_env @@ -71,14 +72,18 @@ def build_preprocessor(self, opts): @staticmethod def prepare_cell_mocks(*messages): + """ + TODO: describe how we're mocking overall with usage example + """ messages = list(messages) def prepared_wrapper(func): @functools.wraps(func) def test_mock_wrapper(self): + """ + TODO: describe and slit shell mock from channel mock into separate funcs + """ parent_id = 'fake_id' - cell_mock = MagicMock(source='"foo" = "bar"', outputs=[]) - # Hack to help catch `.` and `[]` style access to outputs against the same mock object - cell_mock.__getitem__.side_effect = lambda n: cell_mock.outputs if n == 'outputs' else None + cell_mock = NotebookNode(source='"foo" = "bar"', outputs=[]) preprocessor = self.build_preprocessor({}) preprocessor.nb = {'cells': [cell_mock]} shell_message_mock = MagicMock( @@ -92,7 +97,9 @@ def test_mock_wrapper(self): for msg in messages ] ) + # self.kc.iopub_channel.get_msg => message_mock.side_effect[i] channel_mock = MagicMock(get_msg=message_mock) + # self.kc.iopub_channel.get_msg => message_mock.side_effect[i] shell_mock = MagicMock(get_msg=shell_message_mock) preprocessor.kc = MagicMock( iopub_channel=channel_mock, @@ -403,7 +410,7 @@ def test_idle_message(self, preprocessor, cell_mock, message_mock): # Just the exit message should be fetched message_mock.assert_called_once() # Ensure no outputs were generated - self.assertFalse(cell_mock.outputs) + assert cell_mock.outputs == [] @ExecuteTestBase.prepare_cell_mocks({ 'msg_type': 'stream', @@ -414,9 +421,9 @@ def test_idle_message(self, preprocessor, cell_mock, message_mock): def test_message_for_wrong_parent(self, preprocessor, cell_mock, message_mock): preprocessor.run_cell(cell_mock) # An ignored stream followed by an idle - self.assertEqual(message_mock.call_count, 2) + assert message_mock.call_count == 2 # Ensure no output was written - self.assertFalse(cell_mock.outputs) + assert cell_mock.outputs == [] @ExecuteTestBase.prepare_cell_mocks({ 'msg_type': 'status', @@ -426,9 +433,9 @@ def test_message_for_wrong_parent(self, preprocessor, cell_mock, message_mock): def test_busy_message(self, preprocessor, cell_mock, message_mock): preprocessor.run_cell(cell_mock) # One busy message, followed by an idle - self.assertEqual(message_mock.call_count, 2) + assert message_mock.call_count == 2 # Ensure no outputs were generated - self.assertFalse(cell_mock.outputs) + assert cell_mock.outputs == [] @ExecuteTestBase.prepare_cell_mocks({ 'msg_type': 'execute_input', @@ -438,9 +445,9 @@ def test_busy_message(self, preprocessor, cell_mock, message_mock): def test_execute_input_message(self, preprocessor, cell_mock, message_mock): preprocessor.run_cell(cell_mock) # One ignored execute_input, followed by an idle - self.assertEqual(message_mock.call_count, 2) + assert message_mock.call_count == 2 # Ensure no outputs were generated - self.assertFalse(cell_mock.outputs) + assert cell_mock.outputs == [] @ExecuteTestBase.prepare_cell_mocks({ 'msg_type': 'stream', @@ -454,7 +461,7 @@ def test_execute_input_message(self, preprocessor, cell_mock, message_mock): def test_stream_messages(self, preprocessor, cell_mock, message_mock): preprocessor.run_cell(cell_mock) # An stdout then stderr stream followed by an idle - self.assertEqual(message_mock.call_count, 3) + assert message_mock.call_count == 3 # Ensure the output was captured self.assertListEqual(cell_mock.outputs, [ {'output_type': 'stream', 'name': 'stdout', 'text': 'foo'}, @@ -473,9 +480,9 @@ def test_stream_messages(self, preprocessor, cell_mock, message_mock): def test_clear_output_message(self, preprocessor, cell_mock, message_mock): preprocessor.run_cell(cell_mock) # A stream, followed by a clear, and then an idle - self.assertEqual(message_mock.call_count, 3) + assert message_mock.call_count == 3 # Ensure the output was cleared - self.assertFalse(cell_mock.outputs) + assert cell_mock.outputs == [] @ExecuteTestBase.prepare_cell_mocks({ 'msg_type': 'stream', @@ -489,13 +496,13 @@ def test_clear_output_message(self, preprocessor, cell_mock, message_mock): def test_clear_output_wait_message(self, preprocessor, cell_mock, message_mock): preprocessor.run_cell(cell_mock) # A stream, followed by a clear, and then an idle - self.assertEqual(message_mock.call_count, 3) + assert message_mock.call_count == 3 # Should be true without another message to trigger the clear self.assertTrue(preprocessor.clear_before_next_output) # Ensure the output wasn't cleared yet - self.assertListEqual(cell_mock.outputs, [ + assert cell_mock.outputs == [ {'output_type': 'stream', 'name': 'stdout', 'text': 'foo'} - ]) + ] @ExecuteTestBase.prepare_cell_mocks({ 'msg_type': 'stream', @@ -513,13 +520,37 @@ def test_clear_output_wait_message(self, preprocessor, cell_mock, message_mock): def test_clear_output_wait_then_message_message(self, preprocessor, cell_mock, message_mock): preprocessor.run_cell(cell_mock) # An stdout stream, followed by a wait clear, an stderr stream, and then an idle - self.assertEqual(message_mock.call_count, 4) + assert message_mock.call_count == 4 # Should be false after the stderr message - self.assertFalse(preprocessor.clear_before_next_output) + assert not preprocessor.clear_before_next_output # Ensure the output wasn't cleared yet - self.assertListEqual(cell_mock.outputs, [ + assert cell_mock.outputs == [ {'output_type': 'stream', 'name': 'stderr', 'text': 'bar'} - ]) + ] + + @ExecuteTestBase.prepare_cell_mocks({ + 'msg_type': 'stream', + 'header': {'msg_type': 'stream'}, + 'content': {'name': 'stdout', 'text': 'foo'} + }, { + 'msg_type': 'clear_output', + 'header': {'msg_type': 'clear_output'}, + 'content': {'wait': True} + }, { + 'msg_type': 'update_display_data', + 'header': {'msg_type': 'update_display_data'}, + 'content': {'metadata': {'metafoo': 'metabar'}, 'data': {'foo': 'bar'}} + }) + def test_clear_output_wait_then_update_display_message(self, preprocessor, cell_mock, message_mock): + preprocessor.run_cell(cell_mock) + # An stdout stream, followed by a wait clear, an stderr stream, and then an idle + assert message_mock.call_count == 4 + # Should be false after the stderr message + assert preprocessor.clear_before_next_output + # Ensure the output wasn't cleared yet because update_display doesn't add outputs + assert cell_mock.outputs == [ + {'output_type': 'stream', 'name': 'stdout', 'text': 'foo'} + ] @ExecuteTestBase.prepare_cell_mocks({ 'msg_type': 'execute_reply', @@ -529,10 +560,10 @@ def test_clear_output_wait_then_message_message(self, preprocessor, cell_mock, m def test_execution_count_message(self, preprocessor, cell_mock, message_mock): preprocessor.run_cell(cell_mock) # An execution count followed by an idle - self.assertEqual(message_mock.call_count, 2) - cell_mock.__setitem__.assert_called_once_with('execution_count', 42) + assert message_mock.call_count == 2 + assert cell_mock.execution_count == 42 # Ensure no outputs were generated - self.assertFalse(cell_mock.outputs) + assert cell_mock.outputs == [] @ExecuteTestBase.prepare_cell_mocks({ 'msg_type': 'stream', @@ -542,12 +573,12 @@ def test_execution_count_message(self, preprocessor, cell_mock, message_mock): def test_execution_count_with_stream_message(self, preprocessor, cell_mock, message_mock): preprocessor.run_cell(cell_mock) # An execution count followed by an idle - self.assertEqual(message_mock.call_count, 2) - cell_mock.__setitem__.assert_called_once_with('execution_count', 42) + assert message_mock.call_count == 2 + assert cell_mock.execution_count == 42 # Should also consume the message stream - self.assertListEqual(cell_mock.outputs, [ + assert cell_mock.outputs == [ {'output_type': 'stream', 'name': 'stdout', 'text': 'foo'} - ]) + ] @ExecuteTestBase.prepare_cell_mocks({ 'msg_type': 'comm', @@ -560,12 +591,12 @@ def test_execution_count_with_stream_message(self, preprocessor, cell_mock, mess def test_widget_comm_message(self, preprocessor, cell_mock, message_mock): preprocessor.run_cell(cell_mock) # A comm message without buffer info followed by an idle - self.assertEqual(message_mock.call_count, 2) + assert message_mock.call_count == 2 self.assertEqual(preprocessor.widget_state, {'foobar': {'foo': 'bar'}}) # Buffers should still be empty - self.assertFalse(preprocessor.widget_buffers) + assert not preprocessor.widget_buffers # Ensure no outputs were generated - self.assertFalse(cell_mock.outputs) + assert cell_mock.outputs == [] @ExecuteTestBase.prepare_cell_mocks({ 'msg_type': 'comm', @@ -582,13 +613,13 @@ def test_widget_comm_message(self, preprocessor, cell_mock, message_mock): def test_widget_comm_buffer_message(self, preprocessor, cell_mock, message_mock): preprocessor.run_cell(cell_mock) # A comm message with buffer info followed by an idle - self.assertEqual(message_mock.call_count, 2) - self.assertEqual(preprocessor.widget_state, {'foobar': {'foo': 'bar'}}) - self.assertEqual(preprocessor.widget_buffers, - {'foobar': [{'data': 'MTIz', 'encoding': 'base64', 'path': 'path'}]} - ) + assert message_mock.call_count == 2 + assert preprocessor.widget_state == {'foobar': {'foo': 'bar'}} + assert preprocessor.widget_buffers == { + 'foobar': [{'data': 'MTIz', 'encoding': 'base64', 'path': 'path'}] + } # Ensure no outputs were generated - self.assertFalse(cell_mock.outputs) + assert cell_mock.outputs == [] @ExecuteTestBase.prepare_cell_mocks({ 'msg_type': 'comm', @@ -602,12 +633,12 @@ def test_widget_comm_buffer_message(self, preprocessor, cell_mock, message_mock) def test_unknown_comm_message(self, preprocessor, cell_mock, message_mock): preprocessor.run_cell(cell_mock) # An unknown comm message followed by an idle - self.assertEqual(message_mock.call_count, 2) + assert message_mock.call_count == 2 # Widget states should be empty as the message has the wrong shape - self.assertFalse(preprocessor.widget_state) - self.assertFalse(preprocessor.widget_buffers) + assert not preprocessor.widget_state + assert not preprocessor.widget_buffers # Ensure no outputs were generated - self.assertFalse(cell_mock.outputs) + assert cell_mock.outputs == [] @ExecuteTestBase.prepare_cell_mocks({ 'msg_type': 'execute_result', @@ -621,17 +652,17 @@ def test_unknown_comm_message(self, preprocessor, cell_mock, message_mock): def test_execute_result_message(self, preprocessor, cell_mock, message_mock): preprocessor.run_cell(cell_mock) # An execute followed by an idle - self.assertEqual(message_mock.call_count, 2) - cell_mock.__setitem__.assert_called_once_with('execution_count', 42) + assert message_mock.call_count == 2 + assert cell_mock.execution_count == 42 # Should generate an associated message - self.assertListEqual(cell_mock.outputs, [{ + assert cell_mock.outputs == [{ 'output_type': 'execute_result', 'metadata': {'metafoo': 'metabar'}, 'data': {'foo': 'bar'}, 'execution_count': 42 - }]) + }] # No display id was provided - self.assertFalse(preprocessor._display_id_map) + assert not preprocessor._display_id_map @ExecuteTestBase.prepare_cell_mocks({ 'msg_type': 'execute_result', @@ -646,16 +677,16 @@ def test_execute_result_message(self, preprocessor, cell_mock, message_mock): def test_execute_result_with_display_message(self, preprocessor, cell_mock, message_mock): preprocessor.run_cell(cell_mock) # An execute followed by an idle - self.assertEqual(message_mock.call_count, 2) - cell_mock.__setitem__.assert_called_once_with('execution_count', 42) + assert message_mock.call_count == 2 + assert cell_mock.execution_count == 42 # Should generate an associated message - self.assertListEqual(cell_mock.outputs, [{ + assert cell_mock.outputs == [{ 'output_type': 'execute_result', 'metadata': {'metafoo': 'metabar'}, 'data': {'foo': 'bar'}, 'execution_count': 42 - }]) - self.assertTrue('foobar' in preprocessor._display_id_map) + }] + assert 'foobar' in preprocessor._display_id_map @ExecuteTestBase.prepare_cell_mocks({ 'msg_type': 'display_data', @@ -665,15 +696,15 @@ def test_execute_result_with_display_message(self, preprocessor, cell_mock, mess def test_display_data_without_id_message(self, preprocessor, cell_mock, message_mock): preprocessor.run_cell(cell_mock) # A display followed by an idle - self.assertEqual(message_mock.call_count, 2) + assert message_mock.call_count == 2 # Should generate an associated message - self.assertListEqual(cell_mock.outputs, [{ + assert cell_mock.outputs == [{ 'output_type': 'display_data', 'metadata': {'metafoo': 'metabar'}, 'data': {'foo': 'bar'} - }]) + }] # No display id was provided - self.assertFalse(preprocessor._display_id_map) + assert not preprocessor._display_id_map @ExecuteTestBase.prepare_cell_mocks({ 'msg_type': 'display_data', @@ -687,14 +718,14 @@ def test_display_data_without_id_message(self, preprocessor, cell_mock, message_ def test_display_data_message(self, preprocessor, cell_mock, message_mock): preprocessor.run_cell(cell_mock) # A display followed by an idle - self.assertEqual(message_mock.call_count, 2) + assert message_mock.call_count == 2 # Should generate an associated message - self.assertListEqual(cell_mock.outputs, [{ + assert cell_mock.outputs == [{ 'output_type': 'display_data', 'metadata': {'metafoo': 'metabar'}, 'data': {'foo': 'bar'} - }]) - self.assertTrue('foobar' in preprocessor._display_id_map) + }] + assert 'foobar' in preprocessor._display_id_map @ExecuteTestBase.prepare_cell_mocks({ 'msg_type': 'display_data', @@ -704,6 +735,14 @@ def test_display_data_message(self, preprocessor, cell_mock, message_mock): 'metadata': {'metafoo': 'metabar'}, 'data': {'foo': 'bar'} } + }, { + 'msg_type': 'display_data', + 'header': {'msg_type': 'display_data'}, + 'content': { + 'transient': {'display_id': 'foobar_other'}, + 'metadata': {'metafoo_other': 'metabar_other'}, + 'data': {'foo': 'bar_other'} + } }, { 'msg_type': 'display_data', 'header': {'msg_type': 'display_data'}, @@ -716,18 +755,22 @@ def test_display_data_message(self, preprocessor, cell_mock, message_mock): def test_display_data_same_id_message(self, preprocessor, cell_mock, message_mock): preprocessor.run_cell(cell_mock) # A display followed by an idle - self.assertEqual(message_mock.call_count, 3) + assert message_mock.call_count == 4 # Original output should be manipulated and a copy of the second now - self.assertListEqual(cell_mock.outputs, [{ + assert cell_mock.outputs == [{ 'output_type': 'display_data', 'metadata': {'metafoo2': 'metabar2'}, 'data': {'foo': 'bar2', 'baz': 'foobarbaz'} + }, { + 'output_type': 'display_data', + 'metadata': {'metafoo_other': 'metabar_other'}, + 'data': {'foo': 'bar_other'} }, { 'output_type': 'display_data', 'metadata': {'metafoo2': 'metabar2'}, 'data': {'foo': 'bar2', 'baz': 'foobarbaz'} - }]) - self.assertTrue('foobar' in preprocessor._display_id_map) + }] + assert 'foobar' in preprocessor._display_id_map @ExecuteTestBase.prepare_cell_mocks({ 'msg_type': 'update_display_data', @@ -737,17 +780,25 @@ def test_display_data_same_id_message(self, preprocessor, cell_mock, message_moc def test_update_display_data_without_id_message(self, preprocessor, cell_mock, message_mock): preprocessor.run_cell(cell_mock) # An update followed by an idle - self.assertEqual(message_mock.call_count, 2) + assert message_mock.call_count == 2 # Display updates don't create any outputs - self.assertFalse(cell_mock.outputs) + assert cell_mock.outputs == [] # No display id was provided - self.assertFalse(preprocessor._display_id_map) + assert not preprocessor._display_id_map @ExecuteTestBase.prepare_cell_mocks({ + 'msg_type': 'display_data', + 'header': {'msg_type': 'display_data'}, + 'content': { + 'transient': {'display_id': 'foobar'}, + 'metadata': {'metafoo2': 'metabar2'}, + 'data': {'foo': 'bar2', 'baz': 'foobarbaz'} + } + }, { 'msg_type': 'update_display_data', 'header': {'msg_type': 'update_display_data'}, 'content': { - 'transient': {'display_id': 'foobar'}, + 'transient': {'display_id': 'foobar2'}, 'metadata': {'metafoo2': 'metabar2'}, 'data': {'foo': 'bar2', 'baz': 'foobarbaz'} } @@ -755,11 +806,14 @@ def test_update_display_data_without_id_message(self, preprocessor, cell_mock, m def test_update_display_data_mismatch_id_message(self, preprocessor, cell_mock, message_mock): preprocessor.run_cell(cell_mock) # An update followed by an idle - self.assertEqual(message_mock.call_count, 2) + assert message_mock.call_count == 3 # Display updates don't create any outputs - self.assertFalse(cell_mock.outputs) - # Display id wasn't found, so message was skipped - self.assertFalse(preprocessor._display_id_map) + assert cell_mock.outputs == [{ + 'output_type': 'display_data', + 'metadata': {'metafoo2': 'metabar2'}, + 'data': {'foo': 'bar2', 'baz': 'foobarbaz'} + }] + assert 'foobar' in preprocessor._display_id_map @ExecuteTestBase.prepare_cell_mocks({ 'msg_type': 'display_data', @@ -781,14 +835,14 @@ def test_update_display_data_mismatch_id_message(self, preprocessor, cell_mock, def test_update_display_data_message(self, preprocessor, cell_mock, message_mock): preprocessor.run_cell(cell_mock) # A display followed by an update then an idle - self.assertEqual(message_mock.call_count, 3) + assert message_mock.call_count == 3 # Original output should be manipulated - self.assertListEqual(cell_mock.outputs, [{ + assert cell_mock.outputs == [{ 'output_type': 'display_data', 'metadata': {'metafoo2': 'metabar2'}, 'data': {'foo': 'bar2', 'baz': 'foobarbaz'} - }]) - self.assertTrue('foobar' in preprocessor._display_id_map) + }] + assert 'foobar' in preprocessor._display_id_map @ExecuteTestBase.prepare_cell_mocks({ 'msg_type': 'error', @@ -798,11 +852,11 @@ def test_update_display_data_message(self, preprocessor, cell_mock, message_mock def test_error_message(self, preprocessor, cell_mock, message_mock): preprocessor.run_cell(cell_mock) # An error followed by an idle - self.assertEqual(message_mock.call_count, 2) + assert message_mock.call_count == 2 # Should also consume the message stream - self.assertListEqual(cell_mock.outputs, [{ + assert cell_mock.outputs == [{ 'output_type': 'error', 'ename': 'foo', 'evalue': 'bar', 'traceback': ['Boom'] - }]) + }] From 3dcaf40ec6d3a623e0507f454a5956f88a5109bc Mon Sep 17 00:00:00 2001 From: Matthew Seal Date: Mon, 15 Apr 2019 19:07:22 -0700 Subject: [PATCH 252/671] Filled in remaining TODO items --- nbconvert/preprocessors/tests/test_execute.py | 78 ++++++++++++++----- 1 file changed, 59 insertions(+), 19 deletions(-) diff --git a/nbconvert/preprocessors/tests/test_execute.py b/nbconvert/preprocessors/tests/test_execute.py index 1af5730bf..876d19408 100644 --- a/nbconvert/preprocessors/tests/test_execute.py +++ b/nbconvert/preprocessors/tests/test_execute.py @@ -73,37 +73,77 @@ def build_preprocessor(self, opts): @staticmethod def prepare_cell_mocks(*messages): """ - TODO: describe how we're mocking overall with usage example + This function prepares a preprocessor object which has a fake kernel client + to mock the messages sent over zeromq. The mock kernel client will return + the messages passed into this wrapper back from `preproc.kc.iopub_channel.get_msg` + callbacks. It also appends a kernel idle message to the end of messages. + + This allows for testing in with following call expectations: + + @ExecuteTestBase.prepare_cell_mocks({ + 'msg_type': 'stream', + 'header': {'msg_type': 'stream'}, + 'content': {'name': 'stdout', 'text': 'foo'}, + }) + def test_message_foo(self, preprocessor, cell_mock, message_mock): + preprocessor.kc.iopub_channel.get_msg() + # => + # { + # 'msg_type': 'stream', + # 'parent_header': {'msg_id': 'fake_id'}, + # 'header': {'msg_type': 'stream'}, + # 'content': {'name': 'stdout', 'text': 'foo'}, + # } + preprocessor.kc.iopub_channel.get_msg() + # => + # { + # 'msg_type': 'status', + # 'parent_header': {'msg_id': 'fake_id'}, + # 'content': {'execution_state': 'idle'}, + # } + preprocessor.kc.iopub_channel.get_msg() # => None + message_mock.call_count # => 3 """ + parent_id = 'fake_id' messages = list(messages) + # Always terminate messages with an idle to exit the loop + messages.append({'msg_type': 'status', 'content': {'execution_state': 'idle'}}) + + def shell_channel_message_mock(): + # Return the message generator for + # self.kc.shell_channel.get_msg => {'parent_header': {'msg_id': parent_id}} + return MagicMock(return_value={'parent_header': {'msg_id': parent_id}}) + + def iopub_messages_mock(): + # Return the message generator for + # self.kc.iopub_channel.get_msg => messages[i] + return MagicMock( + side_effect=[ + # Default the parent_header so mocks don't need to include this + merge_dicts({'parent_header': {'msg_id': parent_id}}, msg) + for msg in messages + ] + ) + def prepared_wrapper(func): @functools.wraps(func) def test_mock_wrapper(self): """ - TODO: describe and slit shell mock from channel mock into separate funcs + This inner function wrapper populates the preprocessor object with + the fake kernel client. This client has it's iopub and shell + channels mocked so as to fake the setup handshake and return + the messages passed into prepare_cell_mocks as the run_cell loop + processes them. """ - parent_id = 'fake_id' cell_mock = NotebookNode(source='"foo" = "bar"', outputs=[]) preprocessor = self.build_preprocessor({}) preprocessor.nb = {'cells': [cell_mock]} - shell_message_mock = MagicMock( - return_value={'parent_header': {'msg_id': parent_id}}) - # Always terminate messages with an idle to exit the loop - messages.append({'msg_type': 'status', 'content': {'execution_state': 'idle'}}) - message_mock = MagicMock( - side_effect=[ - # Default the parent_header so mocks don't need to include this - merge_dicts({'parent_header': {'msg_id': parent_id}}, msg) - for msg in messages - ] - ) - # self.kc.iopub_channel.get_msg => message_mock.side_effect[i] - channel_mock = MagicMock(get_msg=message_mock) + # self.kc.iopub_channel.get_msg => message_mock.side_effect[i] - shell_mock = MagicMock(get_msg=shell_message_mock) + message_mock = iopub_messages_mock() preprocessor.kc = MagicMock( - iopub_channel=channel_mock, - shell_channel=shell_mock, + iopub_channel=MagicMock(get_msg=message_mock), + shell_channel=MagicMock(get_msg=shell_channel_message_mock()), execute=MagicMock(return_value=parent_id) ) return func(self, preprocessor, cell_mock, message_mock) From d06295b2a91fc9b2f57c8e3b8761eaa34a200be6 Mon Sep 17 00:00:00 2001 From: Matthew Seal Date: Mon, 15 Apr 2019 19:15:15 -0700 Subject: [PATCH 253/671] Applied last of PR feedback --- nbconvert/preprocessors/tests/test_execute.py | 16 ++++++---------- nbconvert/tests/base.py | 8 ++++++++ 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/nbconvert/preprocessors/tests/test_execute.py b/nbconvert/preprocessors/tests/test_execute.py index 876d19408..2e4cff03f 100644 --- a/nbconvert/preprocessors/tests/test_execute.py +++ b/nbconvert/preprocessors/tests/test_execute.py @@ -23,7 +23,6 @@ from ..execute import ExecutePreprocessor, CellExecutionError, executenb import IPython -from mock import patch, MagicMock from traitlets import TraitError from nbformat import NotebookNode from jupyter_client.kernelspec import KernelSpecManager @@ -35,6 +34,10 @@ TimeoutError # Py 3 except NameError: TimeoutError = RuntimeError # Py 2 +try: + from unittest.mock import MagicMock, patch # Py 3 +except ImportError: + from mock import MagicMock, patch # Py 2 addr_pat = re.compile(r'0x[0-9a-f]{7,9}') ipython_input_pat = re.compile(r'') @@ -49,14 +52,6 @@ def _normalize_base64(b64_text): return b64_text -def merge_dicts(first, second): - # Because this is annoying to do inline - outcome = {} - outcome.update(first) - outcome.update(second) - return outcome - - class ExecuteTestBase(PreprocessorTestsBase): def build_preprocessor(self, opts): """Make an instance of a preprocessor""" @@ -120,7 +115,8 @@ def iopub_messages_mock(): return MagicMock( side_effect=[ # Default the parent_header so mocks don't need to include this - merge_dicts({'parent_header': {'msg_id': parent_id}}, msg) + ExecuteTestBase.merge_dicts( + {'parent_header': {'msg_id': parent_id}}, msg) for msg in messages ] ) diff --git a/nbconvert/tests/base.py b/nbconvert/tests/base.py index 383c94b6c..ee39dd2ca 100644 --- a/nbconvert/tests/base.py +++ b/nbconvert/tests/base.py @@ -98,6 +98,14 @@ def create_temp_cwd(self, copy_filenames=None): #Return directory handler return temp_dir + @classmethod + def merge_dicts(cls, *dict_args): + # Because this is annoying to do inline + outcome = {} + for d in dict_args: + outcome.update(d) + return outcome + def create_empty_notebook(self, path): nb = v4.new_notebook() with io.open(path, 'w', encoding='utf-8') as f: From ed25e8f6c608a2902e58c93941b0ebf96a4412ea Mon Sep 17 00:00:00 2001 From: Matthew Seal Date: Tue, 16 Apr 2019 10:16:06 -0700 Subject: [PATCH 254/671] Fixed test_execute mock for python 3.5 --- nbconvert/preprocessors/tests/test_execute.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nbconvert/preprocessors/tests/test_execute.py b/nbconvert/preprocessors/tests/test_execute.py index 2e4cff03f..0ea7519a9 100644 --- a/nbconvert/preprocessors/tests/test_execute.py +++ b/nbconvert/preprocessors/tests/test_execute.py @@ -444,7 +444,7 @@ class TestRunCell(ExecuteTestBase): def test_idle_message(self, preprocessor, cell_mock, message_mock): preprocessor.run_cell(cell_mock) # Just the exit message should be fetched - message_mock.assert_called_once() + assert message_mock.call_count == 1 # Ensure no outputs were generated assert cell_mock.outputs == [] From 72bac112716218dc71b642cbe6c1b8d190d429af Mon Sep 17 00:00:00 2001 From: Matthew Seal Date: Sat, 6 Apr 2019 21:35:37 -0700 Subject: [PATCH 255/671] Added tests for each branch in execute's run_cell method --- nbconvert/preprocessors/tests/test_execute.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nbconvert/preprocessors/tests/test_execute.py b/nbconvert/preprocessors/tests/test_execute.py index 0ea7519a9..2af67fb17 100644 --- a/nbconvert/preprocessors/tests/test_execute.py +++ b/nbconvert/preprocessors/tests/test_execute.py @@ -23,6 +23,7 @@ from ..execute import ExecutePreprocessor, CellExecutionError, executenb import IPython +from mock import MagicMock from traitlets import TraitError from nbformat import NotebookNode from jupyter_client.kernelspec import KernelSpecManager @@ -51,7 +52,6 @@ def _normalize_base64(b64_text): except (ValueError, TypeError): return b64_text - class ExecuteTestBase(PreprocessorTestsBase): def build_preprocessor(self, opts): """Make an instance of a preprocessor""" From 6ed3f0bcc19f9a73686a481d93bb6f8477de2906 Mon Sep 17 00:00:00 2001 From: Matthew Seal Date: Sun, 28 Oct 2018 16:56:01 -0700 Subject: [PATCH 256/671] Refactored execute preprocessor to have a process_message function for each kernel message --- nbconvert/preprocessors/execute.py | 74 +++++++++++++++++------------- 1 file changed, 42 insertions(+), 32 deletions(-) diff --git a/nbconvert/preprocessors/execute.py b/nbconvert/preprocessors/execute.py index 01c8a8120..569887acb 100644 --- a/nbconvert/preprocessors/execute.py +++ b/nbconvert/preprocessors/execute.py @@ -488,7 +488,7 @@ def run_cell(self, cell, cell_index=0): self.log.debug("Executing cell:\n%s", cell.source) exec_reply = self._wait_for_reply(parent_msg_id, cell) - outs = cell.outputs = [] + cell.outputs = [] self.clear_before_next_output = False while True: @@ -509,40 +509,50 @@ def run_cell(self, cell, cell_index=0): # not an output from our execution continue - msg_type = msg['msg_type'] - self.log.debug("output: %s", msg_type) - content = msg['content'] + if not self.process_message(msg, cell, cell_index): + break - # set the prompt number for the input and the output - if 'execution_count' in content: - cell['execution_count'] = content['execution_count'] + return exec_reply, cell.outputs - if msg_type == 'status': - if content['execution_state'] == 'idle': - break - else: - continue - elif msg_type == 'execute_input': - continue - elif msg_type == 'clear_output': - self.clear_output(outs, msg, cell_index) - continue - elif msg_type.startswith('comm'): - self.handle_comm_msg(outs, msg, cell_index) - continue - - display_id = None - if msg_type in {'execute_result', 'display_data', 'update_display_data'}: - display_id = msg['content'].get('transient', {}).get('display_id', None) - if display_id: - self._update_display_id(display_id, msg) - if msg_type == 'update_display_data': - # update_display_data doesn't get recorded - continue - - self.output(outs, msg, display_id, cell_index) + def process_message(self, msg, cell, cell_index): + ''' + Returns None if execution should be halted. + ''' + msg_type = msg['msg_type'] + self.log.debug("msg_type: %s", msg_type) + content = msg['content'] + self.log.debug("content: %s", content) + + # Default to our input as the "result" of processing the message + result = msg + + display_id = content.get('transient', {}).get('display_id', None) + if display_id and msg_type in {'execute_result', 'display_data', 'update_display_data'}: + self._update_display_id(display_id, msg) + + # set the prompt number for the input and the output + if 'execution_count' in content: + cell['execution_count'] = content['execution_count'] + + if msg_type == 'status': + if content['execution_state'] == 'idle': + # Set result to None to halt execution + result = None + elif msg_type == 'clear_output': + self.clear_output(cell.outputs, msg, cell_index) + elif msg_type.startswith('comm'): + self.handle_comm_msg(cell.outputs, msg, cell_index) + # Check for remaining messages we don't process + elif not (msg_type in ['execute_input', 'update_display_data'] or msg_type.startswith('comm')): + try: + # Assign output as our processed "result" + result = output_from_msg(msg) + except ValueError: + self.log.error("unhandled iopub msg: " + msg_type) + else: + self.output(cell.outputs, msg, display_id, cell_index) - return exec_reply, outs + return result def output(self, outs, msg, display_id, cell_index): msg_type = msg['msg_type'] From a08dc68e1c2eab80b1f9a8534007177fba40922d Mon Sep 17 00:00:00 2001 From: Matthew Seal Date: Fri, 7 Dec 2018 16:37:09 -0800 Subject: [PATCH 257/671] Added execute wrapper test --- nbconvert/preprocessors/tests/test_execute.py | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/nbconvert/preprocessors/tests/test_execute.py b/nbconvert/preprocessors/tests/test_execute.py index 2af67fb17..35fa7fbd1 100644 --- a/nbconvert/preprocessors/tests/test_execute.py +++ b/nbconvert/preprocessors/tests/test_execute.py @@ -395,6 +395,30 @@ def test_custom_kernel_manager(self): for method, call_count in expected: self.assertNotEqual(call_count, 0, '{} was called'.format(method)) + def test_process_message_wrapper(self): + outputs = [] + + class WrappedPreProc(ExecutePreprocessor): + def process_message(self, msg, cell, cell_index): + result = super(WrappedPreProc, self).process_message(msg, cell, cell_index) + if result and result != msg: + outputs.append(result) + return result + + current_dir = os.path.dirname(__file__) + filename = os.path.join(current_dir, 'files', 'HelloWorld.ipynb') + + with io.open(filename) as f: + input_nb = nbformat.read(f, 4) + + original = copy.deepcopy(input_nb) + wpp = WrappedPreProc() + executed = wpp.preprocess(input_nb, {})[0] + self.assertEqual(outputs, [ + {'name': 'stdout', 'output_type': 'stream', 'text': 'Hello World\n'} + ]) + self.assert_notebooks_equal(original, executed) + def test_execute_function(self): # Test the executenb() convenience API filename = os.path.join(current_dir, 'files', 'HelloWorld.ipynb') From 6cbad47812dece80d1698deef4fa137ce43ef4bb Mon Sep 17 00:00:00 2001 From: Matthew Seal Date: Sun, 14 Apr 2019 19:01:43 -0700 Subject: [PATCH 258/671] Refactored process_message to raise CellExecutionComplete on completion --- nbconvert/preprocessors/execute.py | 77 ++++++++++++------- nbconvert/preprocessors/tests/test_execute.py | 2 +- 2 files changed, 50 insertions(+), 29 deletions(-) diff --git a/nbconvert/preprocessors/execute.py b/nbconvert/preprocessors/execute.py index 569887acb..54df2ab9c 100644 --- a/nbconvert/preprocessors/execute.py +++ b/nbconvert/preprocessors/execute.py @@ -25,6 +25,8 @@ from ..utils.exceptions import ConversionException +class CellExecutionComplete(Exception): pass # Used as a control signal + class CellExecutionError(ConversionException): """ Custom exception to propagate exceptions that are raised during @@ -400,14 +402,13 @@ def preprocess_cell(self, cell, resources, cell_index): if cell.cell_type != 'code' or not cell.source.strip(): return cell, resources - reply, outputs = self.run_cell(cell, cell_index) - cell.outputs = outputs + reply = self.run_cell(cell, cell_index) cell_allows_errors = (self.allow_errors or "raises-exception" in cell.metadata.get("tags", [])) if self.force_raise_errors or not cell_allows_errors: - for out in outputs: + for out in cell.outputs: if out.output_type == 'error': raise CellExecutionError.from_cell_and_msg(cell, out) if (reply is not None) and reply['content']['status'] == 'error': @@ -509,23 +510,46 @@ def run_cell(self, cell, cell_index=0): # not an output from our execution continue - if not self.process_message(msg, cell, cell_index): + # Will raise CellExecutionComplete when completed + try: + self.process_message(msg, cell, cell_index) + except CellExecutionComplete: break - return exec_reply, cell.outputs + return exec_reply def process_message(self, msg, cell, cell_index): - ''' - Returns None if execution should be halted. - ''' + """ + Processes a kernel message, updates cell state, and returns the + resulting output object that was appended to cell.outputs. + + The input argument `cell` is modified in-place. + + Parameters + ---------- + msg : dict + The kernel message being processed. + cell : nbformat.NotebookNode + The cell which is currently being processed. + cell_index : int + The position of the cell within the notebook object. + + Returns + ------- + output : dict + The execution output payload (or None for no output). + + Raises + ------ + CellExecutionComplete + Once a message arrives which indicates computation completeness. + + """ msg_type = msg['msg_type'] self.log.debug("msg_type: %s", msg_type) content = msg['content'] self.log.debug("content: %s", content) - # Default to our input as the "result" of processing the message - result = msg - display_id = content.get('transient', {}).get('display_id', None) if display_id and msg_type in {'execute_result', 'display_data', 'update_display_data'}: self._update_display_id(display_id, msg) @@ -536,45 +560,42 @@ def process_message(self, msg, cell, cell_index): if msg_type == 'status': if content['execution_state'] == 'idle': - # Set result to None to halt execution - result = None + raise CellExecutionComplete() elif msg_type == 'clear_output': self.clear_output(cell.outputs, msg, cell_index) elif msg_type.startswith('comm'): self.handle_comm_msg(cell.outputs, msg, cell_index) # Check for remaining messages we don't process - elif not (msg_type in ['execute_input', 'update_display_data'] or msg_type.startswith('comm')): - try: - # Assign output as our processed "result" - result = output_from_msg(msg) - except ValueError: - self.log.error("unhandled iopub msg: " + msg_type) - else: - self.output(cell.outputs, msg, display_id, cell_index) - - return result + elif msg_type not in ['execute_input', 'update_display_data']: + # Assign output as our processed "result" + return self.output(cell.outputs, msg, display_id, cell_index) def output(self, outs, msg, display_id, cell_index): msg_type = msg['msg_type'] - if self.clear_before_next_output: - self.log.debug('Executing delayed clear_output') - outs[:] = [] - self.clear_display_id_mapping(cell_index) - self.clear_before_next_output = False try: out = output_from_msg(msg) except ValueError: self.log.error("unhandled iopub msg: " + msg_type) return + else: + if self.clear_before_next_output: + self.log.debug('Executing delayed clear_output') + outs[:] = [] + self.clear_display_id_mapping(cell_index) + self.clear_before_next_output = False + if display_id: # record output index in: # _display_id_map[display_id][cell_idx] cell_map = self._display_id_map.setdefault(display_id, {}) output_idx_list = cell_map.setdefault(cell_index, []) output_idx_list.append(len(outs)) + outs.append(out) + return out + def clear_output(self, outs, msg, cell_index): content = msg['content'] if content.get('wait'): diff --git a/nbconvert/preprocessors/tests/test_execute.py b/nbconvert/preprocessors/tests/test_execute.py index 35fa7fbd1..bbfd56b29 100644 --- a/nbconvert/preprocessors/tests/test_execute.py +++ b/nbconvert/preprocessors/tests/test_execute.py @@ -401,7 +401,7 @@ def test_process_message_wrapper(self): class WrappedPreProc(ExecutePreprocessor): def process_message(self, msg, cell, cell_index): result = super(WrappedPreProc, self).process_message(msg, cell, cell_index) - if result and result != msg: + if result: outputs.append(result) return result From 9ed71d747ee5b579fc32a585f83b92e94613c149 Mon Sep 17 00:00:00 2001 From: Matthew Seal Date: Tue, 16 Apr 2019 10:20:27 -0700 Subject: [PATCH 259/671] Changed new asserts to pytest friendlier pattern --- nbconvert/preprocessors/tests/test_execute.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/nbconvert/preprocessors/tests/test_execute.py b/nbconvert/preprocessors/tests/test_execute.py index bbfd56b29..ad8bf82c7 100644 --- a/nbconvert/preprocessors/tests/test_execute.py +++ b/nbconvert/preprocessors/tests/test_execute.py @@ -185,18 +185,18 @@ def normalize_output(output): def assert_notebooks_equal(self, expected, actual): expected_cells = expected['cells'] actual_cells = actual['cells'] - self.assertEqual(len(expected_cells), len(actual_cells)) + assert len(expected_cells) == len(actual_cells) for expected_cell, actual_cell in zip(expected_cells, actual_cells): expected_outputs = expected_cell.get('outputs', []) actual_outputs = actual_cell.get('outputs', []) normalized_expected_outputs = list(map(self.normalize_output, expected_outputs)) normalized_actual_outputs = list(map(self.normalize_output, actual_outputs)) - self.assertEqual(normalized_expected_outputs, normalized_actual_outputs) + assert normalized_expected_outputs == normalized_actual_outputs expected_execution_count = expected_cell.get('execution_count', None) actual_execution_count = actual_cell.get('execution_count', None) - self.assertEqual(expected_execution_count, actual_execution_count) + assert expected_execution_count == actual_execution_count def test_constructor(self): @@ -414,9 +414,9 @@ def process_message(self, msg, cell, cell_index): original = copy.deepcopy(input_nb) wpp = WrappedPreProc() executed = wpp.preprocess(input_nb, {})[0] - self.assertEqual(outputs, [ + assert outputs == [ {'name': 'stdout', 'output_type': 'stream', 'text': 'Hello World\n'} - ]) + ] self.assert_notebooks_equal(original, executed) def test_execute_function(self): From a8b49894e7427f1e4b2d16aa1612f19902468e87 Mon Sep 17 00:00:00 2001 From: Matthew Seal Date: Tue, 16 Apr 2019 11:16:25 -0700 Subject: [PATCH 260/671] Added docstring to control exception --- nbconvert/preprocessors/execute.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/nbconvert/preprocessors/execute.py b/nbconvert/preprocessors/execute.py index 54df2ab9c..89ea8d2c1 100644 --- a/nbconvert/preprocessors/execute.py +++ b/nbconvert/preprocessors/execute.py @@ -25,7 +25,14 @@ from ..utils.exceptions import ConversionException -class CellExecutionComplete(Exception): pass # Used as a control signal +class CellExecutionComplete(Exception): + """ + Used as a control signal for cell execution across run_cell and + process_message function calls. Raised when all execution requests + are completed and no further messages are expected from the kernel + over zeromq channels. + """ + pass class CellExecutionError(ConversionException): """ From 08d2719eb37ebd24c6403b19235cb1b8b9a011a1 Mon Sep 17 00:00:00 2001 From: Dustin H Date: Fri, 19 Apr 2019 19:04:17 -0400 Subject: [PATCH 261/671] first pass at fixing #659 --- nbconvert/preprocessors/execute.py | 196 ++++++++++++++++++----------- 1 file changed, 126 insertions(+), 70 deletions(-) diff --git a/nbconvert/preprocessors/execute.py b/nbconvert/preprocessors/execute.py index 01c8a8120..b09e68ac2 100644 --- a/nbconvert/preprocessors/execute.py +++ b/nbconvert/preprocessors/execute.py @@ -4,6 +4,7 @@ # Copyright (c) IPython Development Team. # Distributed under the terms of the Modified BSD License. import base64 +import time from textwrap import dedent from contextlib import contextmanager @@ -436,46 +437,62 @@ def _update_display_id(self, display_id, msg): outputs[output_idx]['data'] = out['data'] outputs[output_idx]['metadata'] = out['metadata'] + def _poll_for_reply(self, msg_id, cell=None, timeout=None): + try: + # check with timeout if kernel is still alive + msg = self.kc.shell_channel.get_msg(timeout=timeout) + if msg['parent_header'].get('msg_id') == msg_id: + return msg + return None + except Empty: + # received no message, check if kernel is still alive + if not self.kc.is_alive(): + self.log.error( + "Kernel died while waiting for execute reply.") + raise RuntimeError("Kernel died") + # kernel still alive, wait for a message + return None + + def _get_timeout(self, cell): + if self.timeout_func is not None and cell is not None: + timeout = self.timeout_func(cell) + else: + timeout = self.timeout + + if not timeout or timeout < 0: + timeout = None + + return timeout + + def _handle_timeout(self): + self.log.error( + "Timeout waiting for execute reply (%is)." % self.timeout) + if self.interrupt_on_timeout: + self.log.error("Interrupting kernel") + self.km.interrupt_kernel() + return + else: + raise TimeoutError("Cell execution timed out") + def _wait_for_reply(self, msg_id, cell=None): # wait for finish, with timeout while True: try: - if self.timeout_func is not None and cell is not None: - timeout = self.timeout_func(cell) - else: - timeout = self.timeout - - if not timeout or timeout < 0: - timeout = None - + timeout = self._get_timeout(cell) if timeout is not None: # timeout specified msg = self.kc.shell_channel.get_msg(timeout=timeout) else: # no timeout specified, if kernel dies still handle this correctly while True: - try: - # check every few seconds if kernel is still alive - msg = self.kc.shell_channel.get_msg(timeout=5) - except Empty: - # received no message, check if kernel is still alive - if not self.kc.is_alive(): - self.log.error( - "Kernel died while waiting for execute reply.") - raise RuntimeError("Kernel died") - # kernel still alive, wait for a message + msg = self._poll_for_reply(msg_id, cell, 5) + if msg is None: continue # message received break except Empty: - self.log.error( - "Timeout waiting for execute reply (%is)." % self.timeout) - if self.interrupt_on_timeout: - self.log.error("Interrupting kernel") - self.km.interrupt_kernel() - break - else: - raise TimeoutError("Cell execution timed out") + self._handle_timeout() + break if msg['parent_header'].get('msg_id') == msg_id: return msg @@ -483,64 +500,103 @@ def _wait_for_reply(self, msg_id, cell=None): # not our reply continue + def _timeout_with_deadline(self, timeout, deadline): + if deadline is not None and deadline - time.time() < timeout: + timeout = deadline - time.time() + + if timeout < 0: + timeout = 0 + + return timeout + + def _passed_deadline(self, deadline): + if deadline is not None and deadline - time.time() <= 0: + self._handle_timeout() + return True + return False + def run_cell(self, cell, cell_index=0): parent_msg_id = self.kc.execute(cell.source) self.log.debug("Executing cell:\n%s", cell.source) - exec_reply = self._wait_for_reply(parent_msg_id, cell) + exec_timeout = self._get_timeout(cell) + if exec_timeout is not None: + deadline = time.time() + exec_timeout # verify this is correct. (monotonic) outs = cell.outputs = [] self.clear_before_next_output = False - while True: - try: - # We've already waited for execute_reply, so all output - # should already be waiting. However, on slow networks, like - # in certain CI systems, waiting < 1 second might miss messages. - # So long as the kernel sends a status:idle message when it - # finishes, we won't actually have to wait this long, anyway. - msg = self.kc.iopub_channel.get_msg(timeout=self.iopub_timeout) - except Empty: - self.log.warning("Timeout waiting for IOPub output") - if self.raise_on_iopub_timeout: - raise RuntimeError("Timeout waiting for IOPub output") - else: - break - if msg['parent_header'].get('msg_id') != parent_msg_id: - # not an output from our execution - continue + more_output = True + polling_exec_reply = True - msg_type = msg['msg_type'] - self.log.debug("output: %s", msg_type) - content = msg['content'] + while more_output or polling_exec_reply: - # set the prompt number for the input and the output - if 'execution_count' in content: - cell['execution_count'] = content['execution_count'] + if polling_exec_reply: + if self._passed_deadline(deadline): + self._handle_timeout() + polling_exec_reply = False + continue - if msg_type == 'status': - if content['execution_state'] == 'idle': - break - else: + # if we wait 5s, can output still fill up? + # should we poll output first? + timeout = self._timeout_with_deadline(5, deadline) + exec_reply = self._poll_for_reply(parent_msg_id, cell, timeout) + if exec_reply is not None: + polling_exec_reply = False + + if more_output: + try: + timeout = self.iopub_timeout + if polling_exec_reply: + timeout = self._timeout_with_deadline(timeout, deadline) + msg = self.kc.iopub_channel.get_msg(timeout=timeout) + except Empty: + if polling_exec_reply: + continue + + self.log.warning("Timeout waiting for IOPub output") + if self.raise_on_iopub_timeout: + raise RuntimeError("Timeout waiting for IOPub output") + else: + more_output = False + continue + if msg['parent_header'].get('msg_id') != parent_msg_id: + # not an output from our execution continue - elif msg_type == 'execute_input': - continue - elif msg_type == 'clear_output': - self.clear_output(outs, msg, cell_index) - continue - elif msg_type.startswith('comm'): - self.handle_comm_msg(outs, msg, cell_index) - continue - display_id = None - if msg_type in {'execute_result', 'display_data', 'update_display_data'}: - display_id = msg['content'].get('transient', {}).get('display_id', None) - if display_id: - self._update_display_id(display_id, msg) - if msg_type == 'update_display_data': - # update_display_data doesn't get recorded + msg_type = msg['msg_type'] + self.log.debug("output: %s", msg_type) + content = msg['content'] + + # set the prompt number for the input and the output + if 'execution_count' in content: + cell['execution_count'] = content['execution_count'] + + if msg_type == 'status': + if content['execution_state'] == 'idle': + more_output = False + polling_exec_reply = False + continue + else: + continue + elif msg_type == 'execute_input': + continue + elif msg_type == 'clear_output': + self.clear_output(outs, msg, cell_index) continue + elif msg_type.startswith('comm'): + self.handle_comm_msg(outs, msg, cell_index) + continue + + display_id = None + if msg_type in {'execute_result', 'display_data', 'update_display_data'}: + display_id = msg['content'].get('transient', {}).get('display_id', None) + if display_id: + self._update_display_id(display_id, msg) + if msg_type == 'update_display_data': + # update_display_data doesn't get recorded + continue - self.output(outs, msg, display_id, cell_index) + self.output(outs, msg, display_id, cell_index) return exec_reply, outs From 012b4d69bd853ff8eeb39531cf20d03f3c97e086 Mon Sep 17 00:00:00 2001 From: Matthew Seal Date: Fri, 19 Apr 2019 22:19:30 -0700 Subject: [PATCH 262/671] Enabled configuration to be shared to exporters from script exporter --- nbconvert/exporters/script.py | 8 ++++---- nbconvert/exporters/tests/test_script.py | 20 +++++++++++++++++--- 2 files changed, 21 insertions(+), 7 deletions(-) diff --git a/nbconvert/exporters/script.py b/nbconvert/exporters/script.py index 0ef5770b8..fbf3e8fda 100644 --- a/nbconvert/exporters/script.py +++ b/nbconvert/exporters/script.py @@ -15,7 +15,7 @@ class ScriptExporter(TemplateExporter): _exporters = Dict() _lang_exporters = Dict() export_form_notebook = "script" - + @default('template_file') def _template_file_default(self): return 'script.tpl' @@ -33,19 +33,19 @@ def _get_language_exporter(self, lang_name): except entrypoints.NoSuchEntryPoint: self._lang_exporters[lang_name] = None else: - self._lang_exporters[lang_name] = Exporter(parent=self) + self._lang_exporters[lang_name] = Exporter(self.config, parent=self) return self._lang_exporters[lang_name] def from_notebook_node(self, nb, resources=None, **kw): langinfo = nb.metadata.get('language_info', {}) - + # delegate to custom exporter, if specified exporter_name = langinfo.get('nbconvert_exporter') if exporter_name and exporter_name != 'script': self.log.debug("Loading script exporter: %s", exporter_name) if exporter_name not in self._exporters: Exporter = get_exporter(exporter_name) - self._exporters[exporter_name] = Exporter(parent=self) + self._exporters[exporter_name] = Exporter(self.config, parent=self) exporter = self._exporters[exporter_name] return exporter.from_notebook_node(nb, resources, **kw) diff --git a/nbconvert/exporters/tests/test_script.py b/nbconvert/exporters/tests/test_script.py index b8c107494..356a725ee 100644 --- a/nbconvert/exporters/tests/test_script.py +++ b/nbconvert/exporters/tests/test_script.py @@ -26,15 +26,15 @@ def test_export(self): """ScriptExporter can export something""" (output, resources) = self.exporter_class().from_filename(self._get_notebook()) assert len(output) > 0 - + def test_export_python(self): """delegate to custom exporter from language_info""" exporter = self.exporter_class() - + pynb = v4.new_notebook() (output, resources) = self.exporter_class().from_notebook_node(pynb) self.assertNotIn('# coding: utf-8', output) - + pynb.metadata.language_info = { 'name': 'python', 'mimetype': 'text/x-python', @@ -43,6 +43,20 @@ def test_export_python(self): (output, resources) = self.exporter_class().from_notebook_node(pynb) self.assertIn('# coding: utf-8', output) + def test_export_config_transfer(self): + """delegate config to custom exporter from language_info""" + nb = v4.new_notebook() + nb.metadata.language_info = { + 'name': 'python', + 'mimetype': 'text/x-python', + 'nbconvert_exporter': 'python', + } + + exporter = self.exporter_class() + exporter.from_notebook_node(nb) + assert exporter._exporters['python'] != exporter + assert exporter._exporters['python'].config == exporter.config + def test_script_exporter_entrypoint(): nb = v4.new_notebook() nb.metadata.language_info = { From cc45db4fe0c553180c9300a741cf56b8befbcbb0 Mon Sep 17 00:00:00 2001 From: Matthew Seal Date: Sun, 21 Apr 2019 15:40:06 -0700 Subject: [PATCH 263/671] Added some TODO notes for where things were off --- nbconvert/exporters/script.py | 6 ++++-- nbconvert/exporters/templateexporter.py | 1 + 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/nbconvert/exporters/script.py b/nbconvert/exporters/script.py index fbf3e8fda..5c4339857 100644 --- a/nbconvert/exporters/script.py +++ b/nbconvert/exporters/script.py @@ -33,7 +33,8 @@ def _get_language_exporter(self, lang_name): except entrypoints.NoSuchEntryPoint: self._lang_exporters[lang_name] = None else: - self._lang_exporters[lang_name] = Exporter(self.config, parent=self) + # TODO: passing config is wrong, but changing this revealed more complicated issues + self._lang_exporters[lang_name] = Exporter(config=self.config, parent=self) return self._lang_exporters[lang_name] def from_notebook_node(self, nb, resources=None, **kw): @@ -45,7 +46,8 @@ def from_notebook_node(self, nb, resources=None, **kw): self.log.debug("Loading script exporter: %s", exporter_name) if exporter_name not in self._exporters: Exporter = get_exporter(exporter_name) - self._exporters[exporter_name] = Exporter(self.config, parent=self) + # TODO: passing config is wrong, but changing this revealed more complicated issues + self._exporters[exporter_name] = Exporter(config=self.config, parent=self) exporter = self._exporters[exporter_name] return exporter.from_notebook_node(nb, resources, **kw) diff --git a/nbconvert/exporters/templateexporter.py b/nbconvert/exporters/templateexporter.py index df515bc75..bb7f55440 100644 --- a/nbconvert/exporters/templateexporter.py +++ b/nbconvert/exporters/templateexporter.py @@ -239,6 +239,7 @@ def _raw_template_changed(self, change): def _raw_mimetypes_default(self): return [self.output_mimetype, ''] + # TODO: passing config is wrong, but changing this revealed more complicated issues def __init__(self, config=None, **kw): """ Public constructor From 498d8867f6260bb4a548cbf47e07ac667cee9a84 Mon Sep 17 00:00:00 2001 From: Matthew Seal Date: Sun, 21 Apr 2019 16:36:48 -0700 Subject: [PATCH 264/671] Added backwards compatability inheritance fix for run_cell --- nbconvert/preprocessors/execute.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/nbconvert/preprocessors/execute.py b/nbconvert/preprocessors/execute.py index 89ea8d2c1..aac1c11f8 100644 --- a/nbconvert/preprocessors/execute.py +++ b/nbconvert/preprocessors/execute.py @@ -409,7 +409,9 @@ def preprocess_cell(self, cell, resources, cell_index): if cell.cell_type != 'code' or not cell.source.strip(): return cell, resources - reply = self.run_cell(cell, cell_index) + reply, outputs = self.run_cell(cell, cell_index) + # Backwards compatability for processes that wrap run_cell + cell.outputs = outputs cell_allows_errors = (self.allow_errors or "raises-exception" in cell.metadata.get("tags", [])) @@ -523,7 +525,8 @@ def run_cell(self, cell, cell_index=0): except CellExecutionComplete: break - return exec_reply + # Return cell.outputs still for backwards compatability + return exec_reply, cell.outputs def process_message(self, msg, cell, cell_index): """ From 2f9c69724620dbc20c5bee6f604ebc2571945f0d Mon Sep 17 00:00:00 2001 From: Matthew Seal Date: Sun, 21 Apr 2019 16:42:53 -0700 Subject: [PATCH 265/671] Removed unnecessary else --- nbconvert/preprocessors/execute.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/nbconvert/preprocessors/execute.py b/nbconvert/preprocessors/execute.py index aac1c11f8..a4e01c4d3 100644 --- a/nbconvert/preprocessors/execute.py +++ b/nbconvert/preprocessors/execute.py @@ -588,12 +588,12 @@ def output(self, outs, msg, display_id, cell_index): except ValueError: self.log.error("unhandled iopub msg: " + msg_type) return - else: - if self.clear_before_next_output: - self.log.debug('Executing delayed clear_output') - outs[:] = [] - self.clear_display_id_mapping(cell_index) - self.clear_before_next_output = False + + if self.clear_before_next_output: + self.log.debug('Executing delayed clear_output') + outs[:] = [] + self.clear_display_id_mapping(cell_index) + self.clear_before_next_output = False if display_id: # record output index in: From 927b14e86ec93f44704be0a1921dbf126b796613 Mon Sep 17 00:00:00 2001 From: Dustin H Date: Tue, 23 Apr 2019 12:21:38 -0400 Subject: [PATCH 266/671] resolving PR comments --- nbconvert/preprocessors/execute.py | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/nbconvert/preprocessors/execute.py b/nbconvert/preprocessors/execute.py index b09e68ac2..ff4c47f00 100644 --- a/nbconvert/preprocessors/execute.py +++ b/nbconvert/preprocessors/execute.py @@ -443,7 +443,6 @@ def _poll_for_reply(self, msg_id, cell=None, timeout=None): msg = self.kc.shell_channel.get_msg(timeout=timeout) if msg['parent_header'].get('msg_id') == msg_id: return msg - return None except Empty: # received no message, check if kernel is still alive if not self.kc.is_alive(): @@ -451,7 +450,6 @@ def _poll_for_reply(self, msg_id, cell=None, timeout=None): "Kernel died while waiting for execute reply.") raise RuntimeError("Kernel died") # kernel still alive, wait for a message - return None def _get_timeout(self, cell): if self.timeout_func is not None and cell is not None: @@ -486,10 +484,9 @@ def _wait_for_reply(self, msg_id, cell=None): # no timeout specified, if kernel dies still handle this correctly while True: msg = self._poll_for_reply(msg_id, cell, 5) - if msg is None: - continue - # message received - break + if msg is not None: + # message received + break except Empty: self._handle_timeout() break @@ -536,9 +533,7 @@ def run_cell(self, cell, cell_index=0): polling_exec_reply = False continue - # if we wait 5s, can output still fill up? - # should we poll output first? - timeout = self._timeout_with_deadline(5, deadline) + timeout = self._timeout_with_deadline(1, deadline) exec_reply = self._poll_for_reply(parent_msg_id, cell, timeout) if exec_reply is not None: polling_exec_reply = False @@ -553,10 +548,10 @@ def run_cell(self, cell, cell_index=0): if polling_exec_reply: continue - self.log.warning("Timeout waiting for IOPub output") if self.raise_on_iopub_timeout: raise RuntimeError("Timeout waiting for IOPub output") else: + self.log.warning("Timeout waiting for IOPub output") more_output = False continue if msg['parent_header'].get('msg_id') != parent_msg_id: From e2f927870a22c17584e0984e0f7accd210c44a8b Mon Sep 17 00:00:00 2001 From: Dustin H Date: Tue, 23 Apr 2019 13:50:47 -0400 Subject: [PATCH 267/671] re-add missing blank lines --- nbconvert/preprocessors/execute.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/nbconvert/preprocessors/execute.py b/nbconvert/preprocessors/execute.py index d6c024266..8435b542a 100644 --- a/nbconvert/preprocessors/execute.py +++ b/nbconvert/preprocessors/execute.py @@ -581,7 +581,9 @@ def process_message(self, msg, cell, cell_index): """ Processes a kernel message, updates cell state, and returns the resulting output object that was appended to cell.outputs. + The input argument `cell` is modified in-place. + Parameters ---------- msg : dict @@ -590,6 +592,7 @@ def process_message(self, msg, cell, cell_index): The cell which is currently being processed. cell_index : int The position of the cell within the notebook object. + Returns ------- output : dict From 7bdde6c5bb588031e48618bb2eaf3e3b224075ec Mon Sep 17 00:00:00 2001 From: Dustin H Date: Tue, 23 Apr 2019 14:19:11 -0400 Subject: [PATCH 268/671] _passed_deadline already calls _handle_timeout --- nbconvert/preprocessors/execute.py | 1 - 1 file changed, 1 deletion(-) diff --git a/nbconvert/preprocessors/execute.py b/nbconvert/preprocessors/execute.py index 8435b542a..2178059d5 100644 --- a/nbconvert/preprocessors/execute.py +++ b/nbconvert/preprocessors/execute.py @@ -539,7 +539,6 @@ def run_cell(self, cell, cell_index=0): if polling_exec_reply: if self._passed_deadline(deadline): - self._handle_timeout() polling_exec_reply = False continue From 5fd512bdbbef664d7adf9af4049ae868b5bcf9c6 Mon Sep 17 00:00:00 2001 From: Dustin H Date: Tue, 23 Apr 2019 14:48:24 -0400 Subject: [PATCH 269/671] use monotonic time for the deadline --- nbconvert/preprocessors/execute.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/nbconvert/preprocessors/execute.py b/nbconvert/preprocessors/execute.py index 2178059d5..10ff0645b 100644 --- a/nbconvert/preprocessors/execute.py +++ b/nbconvert/preprocessors/execute.py @@ -508,8 +508,8 @@ def _wait_for_reply(self, msg_id, cell=None): continue def _timeout_with_deadline(self, timeout, deadline): - if deadline is not None and deadline - time.time() < timeout: - timeout = deadline - time.time() + if deadline is not None and deadline - time.monotonic() < timeout: + timeout = deadline - time.monotonic() if timeout < 0: timeout = 0 @@ -517,7 +517,7 @@ def _timeout_with_deadline(self, timeout, deadline): return timeout def _passed_deadline(self, deadline): - if deadline is not None and deadline - time.time() <= 0: + if deadline is not None and deadline - time.monotonic() <= 0: self._handle_timeout() return True return False @@ -527,7 +527,7 @@ def run_cell(self, cell, cell_index=0): self.log.debug("Executing cell:\n%s", cell.source) exec_timeout = self._get_timeout(cell) if exec_timeout is not None: - deadline = time.time() + exec_timeout # verify this is correct. (monotonic) + deadline = time.monotonic() + exec_timeout cell.outputs = [] self.clear_before_next_output = False From 65d64581cc2e264b2121594b733bc0d40f9cb9cc Mon Sep 17 00:00:00 2001 From: Tyler Makaro Date: Thu, 18 Apr 2019 19:30:40 -0700 Subject: [PATCH 270/671] add style_jupyter.tplx --- nbconvert/templates/latex/style_jupyter.tplx | 177 +++++++++++++++++++ 1 file changed, 177 insertions(+) create mode 100644 nbconvert/templates/latex/style_jupyter.tplx diff --git a/nbconvert/templates/latex/style_jupyter.tplx b/nbconvert/templates/latex/style_jupyter.tplx new file mode 100644 index 000000000..066b8dac9 --- /dev/null +++ b/nbconvert/templates/latex/style_jupyter.tplx @@ -0,0 +1,177 @@ +((=- IPython input/output style -=)) +((*- extends 'base.tplx' -*)) + +((*- block packages -*)) + \usepackage[breakable]{tcolorbox} + \tcbset{nobeforeafter} % prevents tcolorboxes being placing in paragraphs + \usepackage{float} + \floatplacement{figure}{H} % forces figures to be placed at the correct location + ((( super() ))) +((*- endblock packages -*)) + +((*- block definitions -*)) + ((( super() ))) +% Pygments definitions + (((- resources.latex.pygments_definitions ))) + + % For linebreaks inside Verbatim environment from package fancyvrb. + \makeatletter + \newbox\Wrappedcontinuationbox + \newbox\Wrappedvisiblespacebox + \newcommand*\Wrappedvisiblespace {\textcolor{red}{\textvisiblespace}} + \newcommand*\Wrappedcontinuationsymbol {\textcolor{red}{\llap{\tiny$\m@th\hookrightarrow$}}} + \newcommand*\Wrappedcontinuationindent {3ex } + \newcommand*\Wrappedafterbreak {\kern\Wrappedcontinuationindent\copy\Wrappedcontinuationbox} + % Take advantage of the already applied Pygments mark-up to insert + % potential linebreaks for TeX processing. + % {, <, #, %, $, ' and ": go to next line. + % _, }, ^, &, >, - and ~: stay at end of broken line. + % Use of \textquotesingle for straight quote. + \newcommand*\Wrappedbreaksatspecials {% + \def\PYGZus{\discretionary{\char`\_}{\Wrappedafterbreak}{\char`\_}}% + \def\PYGZob{\discretionary{}{\Wrappedafterbreak\char`\{}{\char`\{}}% + \def\PYGZcb{\discretionary{\char`\}}{\Wrappedafterbreak}{\char`\}}}% + \def\PYGZca{\discretionary{\char`\^}{\Wrappedafterbreak}{\char`\^}}% + \def\PYGZam{\discretionary{\char`\&}{\Wrappedafterbreak}{\char`\&}}% + \def\PYGZlt{\discretionary{}{\Wrappedafterbreak\char`\<}{\char`\<}}% + \def\PYGZgt{\discretionary{\char`\>}{\Wrappedafterbreak}{\char`\>}}% + \def\PYGZsh{\discretionary{}{\Wrappedafterbreak\char`\#}{\char`\#}}% + \def\PYGZpc{\discretionary{}{\Wrappedafterbreak\char`\%}{\char`\%}}% + \def\PYGZdl{\discretionary{}{\Wrappedafterbreak\char`\$}{\char`\$}}% + \def\PYGZhy{\discretionary{\char`\-}{\Wrappedafterbreak}{\char`\-}}% + \def\PYGZsq{\discretionary{}{\Wrappedafterbreak\textquotesingle}{\textquotesingle}}% + \def\PYGZdq{\discretionary{}{\Wrappedafterbreak\char`\"}{\char`\"}}% + \def\PYGZti{\discretionary{\char`\~}{\Wrappedafterbreak}{\char`\~}}% + } + % Some characters . , ; ? ! / are not pygmentized. + % This macro makes them "active" and they will insert potential linebreaks + \newcommand*\Wrappedbreaksatpunct {% + \lccode`\~`\.\lowercase{\def~}{\discretionary{\hbox{\char`\.}}{\Wrappedafterbreak}{\hbox{\char`\.}}}% + \lccode`\~`\,\lowercase{\def~}{\discretionary{\hbox{\char`\,}}{\Wrappedafterbreak}{\hbox{\char`\,}}}% + \lccode`\~`\;\lowercase{\def~}{\discretionary{\hbox{\char`\;}}{\Wrappedafterbreak}{\hbox{\char`\;}}}% + \lccode`\~`\:\lowercase{\def~}{\discretionary{\hbox{\char`\:}}{\Wrappedafterbreak}{\hbox{\char`\:}}}% + \lccode`\~`\?\lowercase{\def~}{\discretionary{\hbox{\char`\?}}{\Wrappedafterbreak}{\hbox{\char`\?}}}% + \lccode`\~`\!\lowercase{\def~}{\discretionary{\hbox{\char`\!}}{\Wrappedafterbreak}{\hbox{\char`\!}}}% + \lccode`\~`\/\lowercase{\def~}{\discretionary{\hbox{\char`\/}}{\Wrappedafterbreak}{\hbox{\char`\/}}}% + \catcode`\.\active + \catcode`\,\active + \catcode`\;\active + \catcode`\:\active + \catcode`\?\active + \catcode`\!\active + \catcode`\/\active + \lccode`\~`\~ + } + \makeatother + + \let\OriginalVerbatim=\Verbatim + \makeatletter + \renewcommand{\Verbatim}[1][1]{% + %\parskip\z@skip + \sbox\Wrappedcontinuationbox {\Wrappedcontinuationsymbol}% + \sbox\Wrappedvisiblespacebox {\FV@SetupFont\Wrappedvisiblespace}% + \def\FancyVerbFormatLine ##1{\hsize\linewidth + \vtop{\raggedright\hyphenpenalty\z@\exhyphenpenalty\z@ + \doublehyphendemerits\z@\finalhyphendemerits\z@ + \strut ##1\strut}% + }% + % If the linebreak is at a space, the latter will be displayed as visible + % space at end of first line, and a continuation symbol starts next line. + % Stretch/shrink are however usually zero for typewriter font. + \def\FV@Space {% + \nobreak\hskip\z@ plus\fontdimen3\font minus\fontdimen4\font + \discretionary{\copy\Wrappedvisiblespacebox}{\Wrappedafterbreak} + {\kern\fontdimen2\font}% + }% + + % Allow breaks at special characters using \PYG... macros. + \Wrappedbreaksatspecials + % Breaks at punctuation characters . , ; ? ! and / need catcode=\active + \OriginalVerbatim[#1,codes*=\Wrappedbreaksatpunct]% + } + \makeatother + + % Exact colors from NB + ((*- block style_colors *)) + \definecolor{incolor}{HTML}{303F9F} + \definecolor{outcolor}{HTML}{D84315} + \definecolor{cellborder}{HTML}{CFCFCF} + \definecolor{cellbackground}{HTML}{F7F7F7} + ((*- endblock style_colors *)) + + % prompt + ((*- block style_prompt *)) + \newcommand{\prompt}[4]{ + \llap{{\color{#2}[#3]: #4}}\vspace{-1.25em} + } + ((* endblock style_prompt *)) + +((*- endblock definitions -*)) + +%=============================================================================== +% Input +%=============================================================================== + +((* block input scoped *)) + ((( draw_cell(cell.source | highlight_code(strip_verbatim=True), cell, 'In', 'incolor', '\\ ') ))) +((* endblock input *)) + + +%=============================================================================== +% Output +%=============================================================================== + +((*- if charlim is not defined -*)) + ((* set charlim = 80 *)) +((*- endif -*)) + +((* block execute_result scoped *)) + ((*- for type in output.data | filter_data_type -*)) + ((*- if type in ['text/plain']*)) + ((( draw_cell(output.data['text/plain'] | wrap_text(charlim) | escape_latex, cell, 'Out', 'outcolor', '\\ ') ))) + ((* else -*)) + ((( " " ))) + ((( draw_prompt(cell, 'Out', 'outcolor','') )))((( super() ))) + ((*- endif -*)) + ((*- endfor -*)) +((* endblock execute_result *)) + +((* block stream *)) + \begin{Verbatim}[commandchars=\\\{\}] +((( output.text | wrap_text(charlim) | escape_latex | ansi2latex -))) + \end{Verbatim} +((* endblock stream *)) + +%============================================================================== +% Support Macros +%============================================================================== + +% Name: draw_cell +% Purpose: Renders an output/input prompt +((*- if draw_cell is not defined -*)) % Required to allow overriding. +((* macro draw_cell(text, cell, prompt, prompt_color, extra_space) -*)) +((*- if prompt == 'In' -*)) +((*- set style = "breakable, size=fbox, boxrule=1pt, pad at break*=1mm,colback=cellbackground, colframe=cellborder"-*)) +((*- else -*))((*- set style = "breakable, boxrule=.5pt, size=fbox, pad at break*=1mm, opacityfill=0"-*))((*- endif -*)) + +\begin{tcolorbox}[((( style )))] +(((- draw_prompt(cell, prompt, prompt_color, extra_space) ))) +\begin{Verbatim}[commandchars=\\\{\}] +((( text ))) +\end{Verbatim} +\end{tcolorbox} +((*- endmacro *)) +((*- endif -*)) + +% Name: draw_prompt +% Purpose: Renders an output/input prompt +((* macro draw_prompt(cell, prompt, prompt_color, extra_space) -*)) + ((*- if cell.execution_count is defined -*)) + ((*- set execution_count = "" ~ (cell.execution_count | replace(None, " ")) -*)) + ((*- else -*))((*- set execution_count = " " -*))((*- endif *)) + + ((*- if (resources.global_content_filter.include_output_prompt and prompt == 'Out') + or (resources.global_content_filter.include_input_prompt and prompt == 'In' ) *)) +\prompt{(((prompt)))}{(((prompt_color)))}{(((execution_count)))}{(((extra_space)))} + ((*- endif -*)) +((*- endmacro *)) From 4b995a2affb1f790a0f5c9d4831c5e35165558cc Mon Sep 17 00:00:00 2001 From: Tyler Makaro Date: Tue, 23 Apr 2019 10:52:35 -0700 Subject: [PATCH 271/671] make default and spacing adjustments --- nbconvert/templates/latex/article.tplx | 2 +- nbconvert/templates/latex/style_jupyter.tplx | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/nbconvert/templates/latex/article.tplx b/nbconvert/templates/latex/article.tplx index cc2a6a189..7c0f3e673 100644 --- a/nbconvert/templates/latex/article.tplx +++ b/nbconvert/templates/latex/article.tplx @@ -1,7 +1,7 @@ % Default to the notebook output style ((* if not cell_style is defined *)) - ((* set cell_style = 'style_ipython.tplx' *)) + ((* set cell_style = 'style_jupyter.tplx' *)) ((* endif *)) % Inherit from the specified cell style. diff --git a/nbconvert/templates/latex/style_jupyter.tplx b/nbconvert/templates/latex/style_jupyter.tplx index 066b8dac9..fcdfeb957 100644 --- a/nbconvert/templates/latex/style_jupyter.tplx +++ b/nbconvert/templates/latex/style_jupyter.tplx @@ -113,7 +113,7 @@ %=============================================================================== ((* block input scoped *)) - ((( draw_cell(cell.source | highlight_code(strip_verbatim=True), cell, 'In', 'incolor', '\\ ') ))) + ((( draw_cell(cell.source | highlight_code(strip_verbatim=True), cell, 'In', 'incolor', '\\hspace{4pt}') ))) ((* endblock input *)) @@ -128,7 +128,7 @@ ((* block execute_result scoped *)) ((*- for type in output.data | filter_data_type -*)) ((*- if type in ['text/plain']*)) - ((( draw_cell(output.data['text/plain'] | wrap_text(charlim) | escape_latex, cell, 'Out', 'outcolor', '\\ ') ))) + ((( draw_cell(output.data['text/plain'] | wrap_text(charlim) | escape_latex, cell, 'Out', 'outcolor', '\\hspace{3.5pt}') ))) ((* else -*)) ((( " " ))) ((( draw_prompt(cell, 'Out', 'outcolor','') )))((( super() ))) From 28a643fbae9a2731fc2d57903b8fd6a78009cfb3 Mon Sep 17 00:00:00 2001 From: Tyler Makaro Date: Tue, 23 Apr 2019 14:08:35 -0700 Subject: [PATCH 272/671] add tests and change latex comment to jinja comment --- nbconvert/exporters/tests/test_latex.py | 34 ++++++++++++++++++++++++- nbconvert/templates/latex/article.tplx | 4 +-- 2 files changed, 35 insertions(+), 3 deletions(-) diff --git a/nbconvert/exporters/tests/test_latex.py b/nbconvert/exporters/tests/test_latex.py index 6f3b9a9b2..019d07795 100644 --- a/nbconvert/exporters/tests/test_latex.py +++ b/nbconvert/exporters/tests/test_latex.py @@ -114,6 +114,38 @@ def test_prompt_number_color(self): """ (output, resources) = LatexExporter().from_filename( self._get_notebook(nb_name="prompt_numbers.ipynb")) + + in_regex = r"\\prompt\{In\}\{incolor\}\{(\d+|\s*)\}" + out_regex = r"\\prompt\{Out\}\{outcolor\}\{(\d+|\s*)\}" + + ins = ["2", "10", " ", " ", "0"] + outs = ["10"] + + assert re.findall(in_regex, output) == ins + assert re.findall(out_regex, output) == outs + + @onlyif_cmds_exist('pandoc') + def test_prompt_number_color_ipython(self): + """ + Does LatexExporter properly format input and output prompts in color? + + Uses an in memory latex template to load style_ipython as the cell style. + """ + my_loader_tplx = DictLoader({'my_template': + """ + ((* extends 'style_ipython.tplx' *)) + + ((* block docclass *)) + \documentclass[11pt]{article} + ((* endblock docclass *)) + """}) + + class MyExporter(LatexExporter): + template_file = 'my_template' + + (output, resources) = MyExporter(extra_loaders=[my_loader_tplx]).from_filename( + self._get_notebook(nb_name="prompt_numbers.ipynb")) + in_regex = r"In \[\{\\color\{incolor\}(.*)\}\]:" out_regex = r"Out\[\{\\color\{outcolor\}(.*)\}\]:" @@ -122,7 +154,7 @@ def test_prompt_number_color(self): assert re.findall(in_regex, output) == ins assert re.findall(out_regex, output) == outs - + @onlyif_cmds_exist('pandoc') def test_no_prompt_yes_input(self): no_prompt = { diff --git a/nbconvert/templates/latex/article.tplx b/nbconvert/templates/latex/article.tplx index 7c0f3e673..8588c850c 100644 --- a/nbconvert/templates/latex/article.tplx +++ b/nbconvert/templates/latex/article.tplx @@ -1,10 +1,10 @@ -% Default to the notebook output style +((=- Default to the notebook output style -=)) ((* if not cell_style is defined *)) ((* set cell_style = 'style_jupyter.tplx' *)) ((* endif *)) -% Inherit from the specified cell style. +((=- Inherit from the specified cell style. -=)) ((* extends cell_style *)) From b99b758b34f342d6eb098d4a2b710c6e38f56fc4 Mon Sep 17 00:00:00 2001 From: Tyler Makaro Date: Wed, 24 Apr 2019 10:14:02 -0700 Subject: [PATCH 273/671] use newer build environment --- .travis.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 98462c459..f0f1c99d0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,7 +2,7 @@ # https://docs.travis-ci.com/user/trusty-ci-environment/ # needs these two lines: sudo: required -dist: trusty +dist: xenial # required for Python >=3.7 (travis-ci/travis-ci#9069), and defaults to newer texlive install. language: python matrix: @@ -11,7 +11,6 @@ matrix: - python: 3.5 - python: 3.6 - python: 3.7 - dist: xenial # required for Python 3.7 (travis-ci/travis-ci#9069) - python: nightly allow_failures: - python: nightly From 8a112bb7c5cda65280256eb6f1e49c46b379ec17 Mon Sep 17 00:00:00 2001 From: M Pacer Date: Wed, 24 Apr 2019 11:20:58 -0700 Subject: [PATCH 274/671] Creates new test_runtime_kernel_death test This test fails as of this commit. Adds KernelIsDead as child of RuntimeError --- nbconvert/preprocessors/execute.py | 2 ++ nbconvert/preprocessors/tests/test_execute.py | 25 +++++++++++++++++-- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/nbconvert/preprocessors/execute.py b/nbconvert/preprocessors/execute.py index a4e01c4d3..65be9adef 100644 --- a/nbconvert/preprocessors/execute.py +++ b/nbconvert/preprocessors/execute.py @@ -24,6 +24,8 @@ from .base import Preprocessor from ..utils.exceptions import ConversionException +class KernelIsDead(RuntimeError): + pass class CellExecutionComplete(Exception): """ diff --git a/nbconvert/preprocessors/tests/test_execute.py b/nbconvert/preprocessors/tests/test_execute.py index ad8bf82c7..ce796bdee 100644 --- a/nbconvert/preprocessors/tests/test_execute.py +++ b/nbconvert/preprocessors/tests/test_execute.py @@ -20,7 +20,7 @@ import functools from .base import PreprocessorTestsBase -from ..execute import ExecutePreprocessor, CellExecutionError, executenb +from ..execute import ExecutePreprocessor, CellExecutionError, executenb, KernelIsDead import IPython from mock import MagicMock @@ -323,8 +323,29 @@ def timeout_func(source): with pytest.raises(TimeoutError): self.run_notebook(filename, dict(timeout_func=timeout_func), res) + def test_runtime_kernel_death(self): + """Check that an error is raised when the kernel is_alive is false""" + filename = os.path.join(current_dir, 'files', 'Interrupt.ipynb') + with io.open(filename, 'r') as f: + input_nb = nbformat.read(f, 4) + res = self.build_resources() + res['metadata']['path'] = os.path.dirname(filename) + + preprocessor = self.build_preprocessor({"timeout": 5}) + + try: + input_nb, output_nb = preprocessor(input_nb, {}) + except TimeoutError as e: + pass + km, kc = preprocessor.start_new_kernel() + + with patch.object(kc, "is_alive") as alive_mock: + alive_mock.return_value = False + with pytest.raises(KernelIsDead): + input_nb, output_nb = preprocessor.preprocess(input_nb, {}, km=km) + @patch('jupyter_client.KernelManager.is_alive') - def test_kernel_death(self, alive_mock): + def test_startup_kernel_dead(self, alive_mock): """Check that an error is raised when the kernel is_alive is false""" current_dir = os.path.dirname(__file__) filename = os.path.join(current_dir, 'files', 'Interrupt.ipynb') From 0a20cbfefb4375f9bca4cf395ff1a413d51b604d Mon Sep 17 00:00:00 2001 From: M Pacer Date: Wed, 24 Apr 2019 11:32:29 -0700 Subject: [PATCH 275/671] Adds logic to check kernel death status with timeouts Adds private _is_alive method to raise KernelIsDead. Previously this was only being checked circuitously in the codepath where the kernel had no timeout. This should still be abstracted more cleanly but excitedly this works. This does not address the race condition for the test_startup_kernel_dead case. --- nbconvert/preprocessors/execute.py | 51 +++++++++++++++++-- nbconvert/preprocessors/tests/test_execute.py | 2 +- 2 files changed, 47 insertions(+), 6 deletions(-) diff --git a/nbconvert/preprocessors/execute.py b/nbconvert/preprocessors/execute.py index 65be9adef..8be447c31 100644 --- a/nbconvert/preprocessors/execute.py +++ b/nbconvert/preprocessors/execute.py @@ -448,6 +448,13 @@ def _update_display_id(self, display_id, msg): outputs[output_idx]['data'] = out['data'] outputs[output_idx]['metadata'] = out['metadata'] + + def _check_alive(self): + if not self.kc.is_alive(): + self.log.error( + "Kernel died while waiting for execute reply.") + raise KernelIsDead("Kernel died") + def _wait_for_reply(self, msg_id, cell=None): # wait for finish, with timeout while True: @@ -459,10 +466,42 @@ def _wait_for_reply(self, msg_id, cell=None): if not timeout or timeout < 0: timeout = None + + # timeout_interval = 5 + # try: + # msg = self.kc.shell_channel.get_msg(timeout=timeout_interval) + # except Empty: + # self._check_alive() + # cummulative_time += timeout_interval + # if timeout is None or cummulative_time <= timeout: + # continue + # message received + if timeout is not None: # timeout specified - msg = self.kc.shell_channel.get_msg(timeout=timeout) + # msg = self.kc.shell_channel.get_msg(timeout=timeout) + cumm_time = 0 + intervals = 5 + while True: + try: + msg = self.kc.shell_channel.get_msg(timeout=timeout/intervals) + except Empty: + if cumm_time<=timeout: + cumm_time += timeout/intervals + self._check_alive() + continue + else: + self._check_alive() + self.log.error( "Timeout waiting for execute reply (%is)." % self.timeout) + if self.interrupt_on_timeout: + self.log.error("Interrupting kernel") + self.km.interrupt_kernel() + break + else: + raise TimeoutError("Cell execution timed out") + break + else: # no timeout specified, if kernel dies still handle this correctly while True: @@ -471,15 +510,17 @@ def _wait_for_reply(self, msg_id, cell=None): msg = self.kc.shell_channel.get_msg(timeout=5) except Empty: # received no message, check if kernel is still alive - if not self.kc.is_alive(): - self.log.error( - "Kernel died while waiting for execute reply.") - raise RuntimeError("Kernel died") + self._check_alive() + # if not self.kc.is_alive(): + # self.log.error( + # "Kernel died while waiting for execute reply.") + # raise RuntimeError("Kernel died") # kernel still alive, wait for a message continue # message received break except Empty: + self._check_alive() self.log.error( "Timeout waiting for execute reply (%is)." % self.timeout) if self.interrupt_on_timeout: diff --git a/nbconvert/preprocessors/tests/test_execute.py b/nbconvert/preprocessors/tests/test_execute.py index ce796bdee..0834cd1c7 100644 --- a/nbconvert/preprocessors/tests/test_execute.py +++ b/nbconvert/preprocessors/tests/test_execute.py @@ -339,7 +339,7 @@ def test_runtime_kernel_death(self): pass km, kc = preprocessor.start_new_kernel() - with patch.object(kc, "is_alive") as alive_mock: + with patch.object(km, "is_alive") as alive_mock: alive_mock.return_value = False with pytest.raises(KernelIsDead): input_nb, output_nb = preprocessor.preprocess(input_nb, {}, km=km) From 9cccb50f04a6ffbadfbc3f7fcebf21e679027fbf Mon Sep 17 00:00:00 2001 From: Dustin H Date: Wed, 24 Apr 2019 15:43:17 -0400 Subject: [PATCH 276/671] time.monotonic only available in py3 --- nbconvert/preprocessors/execute.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/nbconvert/preprocessors/execute.py b/nbconvert/preprocessors/execute.py index 10ff0645b..64f1067d1 100644 --- a/nbconvert/preprocessors/execute.py +++ b/nbconvert/preprocessors/execute.py @@ -4,9 +4,12 @@ # Copyright (c) IPython Development Team. # Distributed under the terms of the Modified BSD License. import base64 -import time from textwrap import dedent from contextlib import contextmanager +try: + from time import monotonic +except ImportError: + from time import time as monotonic try: from queue import Empty # Py 3 @@ -508,8 +511,8 @@ def _wait_for_reply(self, msg_id, cell=None): continue def _timeout_with_deadline(self, timeout, deadline): - if deadline is not None and deadline - time.monotonic() < timeout: - timeout = deadline - time.monotonic() + if deadline is not None and deadline - monotonic() < timeout: + timeout = deadline - monotonic() if timeout < 0: timeout = 0 @@ -517,7 +520,7 @@ def _timeout_with_deadline(self, timeout, deadline): return timeout def _passed_deadline(self, deadline): - if deadline is not None and deadline - time.monotonic() <= 0: + if deadline is not None and deadline - monotonic() <= 0: self._handle_timeout() return True return False @@ -527,7 +530,7 @@ def run_cell(self, cell, cell_index=0): self.log.debug("Executing cell:\n%s", cell.source) exec_timeout = self._get_timeout(cell) if exec_timeout is not None: - deadline = time.monotonic() + exec_timeout + deadline = monotonic() + exec_timeout cell.outputs = [] self.clear_before_next_output = False From 074d72e609bd544ff4665b488223bf87028ceb2b Mon Sep 17 00:00:00 2001 From: Tyler Makaro Date: Wed, 24 Apr 2019 14:06:59 -0700 Subject: [PATCH 277/671] fix broken link --- docs/source/customizing.ipynb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/customizing.ipynb b/docs/source/customizing.ipynb index 02c68fe45..4da09ebd1 100644 --- a/docs/source/customizing.ipynb +++ b/docs/source/customizing.ipynb @@ -11,7 +11,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Under the hood, nbconvert uses [Jinja templates](https://jinja2.readthedocs.io/en/latest/intro.html) to specify how the notebooks should be formatted. These templates can be fully customized, allowing you to use nbconvert to create notebooks in different formats with different styles as well." + "Under the hood, nbconvert uses [Jinja templates](http://jinja.pocoo.org/docs/latest/) to specify how the notebooks should be formatted. These templates can be fully customized, allowing you to use nbconvert to create notebooks in different formats with different styles as well." ] }, { From 9012bdf93c1afa90f22cecd4b1199b7079bf97c1 Mon Sep 17 00:00:00 2001 From: M Pacer Date: Wed, 24 Apr 2019 15:44:26 -0700 Subject: [PATCH 278/671] clean up execute waiting logic now a new test breaks having to do with the msg not being defined. This makes the codepath much nicer and easier to follow. --- nbconvert/preprocessors/execute.py | 85 ++++++++++-------------------- 1 file changed, 27 insertions(+), 58 deletions(-) diff --git a/nbconvert/preprocessors/execute.py b/nbconvert/preprocessors/execute.py index 8be447c31..bc9eb7dc7 100644 --- a/nbconvert/preprocessors/execute.py +++ b/nbconvert/preprocessors/execute.py @@ -457,68 +457,37 @@ def _check_alive(self): def _wait_for_reply(self, msg_id, cell=None): # wait for finish, with timeout + if self.timeout_func is not None and cell is not None: + timeout = self.timeout_func(cell) + else: + timeout = self.timeout + + if not timeout or timeout < 0: + timeout = None + cummulative_time = 0 + timeout_interval = 5 while True: try: - if self.timeout_func is not None and cell is not None: - timeout = self.timeout_func(cell) - else: - timeout = self.timeout - - if not timeout or timeout < 0: - timeout = None - - # timeout_interval = 5 - # try: - # msg = self.kc.shell_channel.get_msg(timeout=timeout_interval) - # except Empty: - # self._check_alive() - # cummulative_time += timeout_interval - # if timeout is None or cummulative_time <= timeout: - # continue - # message received - - - if timeout is not None: - # timeout specified - # msg = self.kc.shell_channel.get_msg(timeout=timeout) - cumm_time = 0 - intervals = 5 - while True: - try: - msg = self.kc.shell_channel.get_msg(timeout=timeout/intervals) - except Empty: - if cumm_time<=timeout: - cumm_time += timeout/intervals - self._check_alive() - continue - else: - self._check_alive() - self.log.error( "Timeout waiting for execute reply (%is)." % self.timeout) - if self.interrupt_on_timeout: - self.log.error("Interrupting kernel") - self.km.interrupt_kernel() - break - else: - raise TimeoutError("Cell execution timed out") - break - else: - # no timeout specified, if kernel dies still handle this correctly - while True: - try: - # check every few seconds if kernel is still alive - msg = self.kc.shell_channel.get_msg(timeout=5) - except Empty: - # received no message, check if kernel is still alive - self._check_alive() - # if not self.kc.is_alive(): - # self.log.error( - # "Kernel died while waiting for execute reply.") - # raise RuntimeError("Kernel died") - # kernel still alive, wait for a message + while True: + try: + msg = self.kc.shell_channel.get_msg(timeout=timeout_interval) + except Empty: + cummulative_time += timeout_interval + self._check_alive() + if timeout is None or cummulative_time <= timeout: continue - # message received - break + else: + self.log.error( "Timeout waiting for execute reply (%is)." % self.timeout) + if self.interrupt_on_timeout: + self.log.error("Interrupting kernel") + self.km.interrupt_kernel() + break + else: + raise TimeoutError("Cell execution timed out") + break + + except Empty: self._check_alive() self.log.error( From 8e29ec61d61020be2b32d7e5dfe6f8e84c3e9b78 Mon Sep 17 00:00:00 2001 From: M Pacer Date: Wed, 24 Apr 2019 15:55:56 -0700 Subject: [PATCH 279/671] add fix for test_svg to only run if inkscape is available --- nbconvert/exporters/tests/test_latex.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nbconvert/exporters/tests/test_latex.py b/nbconvert/exporters/tests/test_latex.py index 6f3b9a9b2..f8d46c878 100644 --- a/nbconvert/exporters/tests/test_latex.py +++ b/nbconvert/exporters/tests/test_latex.py @@ -139,7 +139,7 @@ def test_no_prompt_yes_input(self): assert "shape" in output assert "evs" in output - @onlyif_cmds_exist('pandoc') + @onlyif_cmds_exist('pandoc', 'inkscape') def test_svg(self): """ Can a LatexExporter export when it recieves raw binary strings form svg? From 6c424d7936538ffaa85253ebe6d651d5531816f0 Mon Sep 17 00:00:00 2001 From: M Pacer Date: Wed, 24 Apr 2019 16:56:06 -0700 Subject: [PATCH 280/671] Simplify the logic for iterating through messages, remove old test kernel death test --- nbconvert/preprocessors/execute.py | 50 ++++++------------- nbconvert/preprocessors/tests/test_execute.py | 19 ++----- 2 files changed, 19 insertions(+), 50 deletions(-) diff --git a/nbconvert/preprocessors/execute.py b/nbconvert/preprocessors/execute.py index bc9eb7dc7..2e432819b 100644 --- a/nbconvert/preprocessors/execute.py +++ b/nbconvert/preprocessors/execute.py @@ -24,7 +24,7 @@ from .base import Preprocessor from ..utils.exceptions import ConversionException -class KernelIsDead(RuntimeError): +class DeadKernelError(RuntimeError): pass class CellExecutionComplete(Exception): @@ -453,7 +453,7 @@ def _check_alive(self): if not self.kc.is_alive(): self.log.error( "Kernel died while waiting for execute reply.") - raise KernelIsDead("Kernel died") + raise DeadKernelError("Kernel died") def _wait_for_reply(self, msg_id, cell=None): # wait for finish, with timeout @@ -468,42 +468,22 @@ def _wait_for_reply(self, msg_id, cell=None): timeout_interval = 5 while True: try: - - while True: - try: - msg = self.kc.shell_channel.get_msg(timeout=timeout_interval) - except Empty: - cummulative_time += timeout_interval - self._check_alive() - if timeout is None or cummulative_time <= timeout: - continue - else: - self.log.error( "Timeout waiting for execute reply (%is)." % self.timeout) - if self.interrupt_on_timeout: - self.log.error("Interrupting kernel") - self.km.interrupt_kernel() - break - else: - raise TimeoutError("Cell execution timed out") - break - - + msg = self.kc.shell_channel.get_msg(timeout=timeout_interval) except Empty: self._check_alive() - self.log.error( - "Timeout waiting for execute reply (%is)." % self.timeout) - if self.interrupt_on_timeout: - self.log.error("Interrupting kernel") - self.km.interrupt_kernel() - break - else: - raise TimeoutError("Cell execution timed out") - - if msg['parent_header'].get('msg_id') == msg_id: - return msg + cummulative_time += timeout_interval + if timeout and cummulative_time > timeout: + self.log.error( + "Timeout waiting for execute reply (%is)." % self.timeout) + if self.interrupt_on_timeout: + self.log.error("Interrupting kernel") + self.km.interrupt_kernel() + break + else: + raise TimeoutError("Cell execution timed out") else: - # not our reply - continue + if msg['parent_header'].get('msg_id') == msg_id: + return msg def run_cell(self, cell, cell_index=0): parent_msg_id = self.kc.execute(cell.source) diff --git a/nbconvert/preprocessors/tests/test_execute.py b/nbconvert/preprocessors/tests/test_execute.py index 0834cd1c7..f441e0511 100644 --- a/nbconvert/preprocessors/tests/test_execute.py +++ b/nbconvert/preprocessors/tests/test_execute.py @@ -20,7 +20,7 @@ import functools from .base import PreprocessorTestsBase -from ..execute import ExecutePreprocessor, CellExecutionError, executenb, KernelIsDead +from ..execute import ExecutePreprocessor, CellExecutionError, executenb, DeadKernelError import IPython from mock import MagicMock @@ -323,7 +323,7 @@ def timeout_func(source): with pytest.raises(TimeoutError): self.run_notebook(filename, dict(timeout_func=timeout_func), res) - def test_runtime_kernel_death(self): + def test_kernel_death(self): """Check that an error is raised when the kernel is_alive is false""" filename = os.path.join(current_dir, 'files', 'Interrupt.ipynb') with io.open(filename, 'r') as f: @@ -335,26 +335,15 @@ def test_runtime_kernel_death(self): try: input_nb, output_nb = preprocessor(input_nb, {}) - except TimeoutError as e: + except TimeoutError: pass km, kc = preprocessor.start_new_kernel() with patch.object(km, "is_alive") as alive_mock: alive_mock.return_value = False - with pytest.raises(KernelIsDead): + with pytest.raises(DeadKernelError): input_nb, output_nb = preprocessor.preprocess(input_nb, {}, km=km) - @patch('jupyter_client.KernelManager.is_alive') - def test_startup_kernel_dead(self, alive_mock): - """Check that an error is raised when the kernel is_alive is false""" - current_dir = os.path.dirname(__file__) - filename = os.path.join(current_dir, 'files', 'Interrupt.ipynb') - res = self.build_resources() - res['metadata']['path'] = os.path.dirname(filename) - - with pytest.raises(RuntimeError): - alive_mock.return_value = False - self.run_notebook(filename, {}, res) def test_allow_errors(self): """ From 966c126674b2cfd45069a93d0acb3988e2c52a9c Mon Sep 17 00:00:00 2001 From: Tyler Makaro Date: Wed, 24 Apr 2019 20:01:20 -0700 Subject: [PATCH 281/671] fix pdf file extension in notebook --- nbconvert/exporters/pdf.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/nbconvert/exporters/pdf.py b/nbconvert/exporters/pdf.py index 20fd0d973..f66d685df 100644 --- a/nbconvert/exporters/pdf.py +++ b/nbconvert/exporters/pdf.py @@ -69,6 +69,10 @@ class PDFExporter(LatexExporter): _captured_output = List() + @default('file_extension') + def _file_extension_default(self): + return '.pdf' + def run_command(self, command_list, filename, count, log_function): """Run command_list count times. From 4b2d61de0470b5ea26e00d05519d917a3084668a Mon Sep 17 00:00:00 2001 From: Tyler Makaro Date: Wed, 24 Apr 2019 20:28:01 -0700 Subject: [PATCH 282/671] fix export_from_notebook names --- nbconvert/exporters/html.py | 1 + nbconvert/exporters/templateexporter.py | 2 -- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/nbconvert/exporters/html.py b/nbconvert/exporters/html.py index 2d2c5d686..53f627cbe 100644 --- a/nbconvert/exporters/html.py +++ b/nbconvert/exporters/html.py @@ -23,6 +23,7 @@ class HTMLExporter(TemplateExporter): custom preprocessors/filters. If you don't need custom preprocessors/ filters, just change the 'template_file' config option. """ + export_from_notebook = "html" anchor_link_text = Unicode(u'¶', help="The text used as the text for anchor links.").tag(config=True) diff --git a/nbconvert/exporters/templateexporter.py b/nbconvert/exporters/templateexporter.py index bb7f55440..77059428c 100644 --- a/nbconvert/exporters/templateexporter.py +++ b/nbconvert/exporters/templateexporter.py @@ -102,8 +102,6 @@ class TemplateExporter(Exporter): _template_cached = None - export_from_notebook = "custom" - def _invalidate_template_cache(self, change=None): self._template_cached = None From 2547642aabbef66ec3685c3c493b32506933c43c Mon Sep 17 00:00:00 2001 From: Tyler Makaro Date: Wed, 24 Apr 2019 20:32:11 -0700 Subject: [PATCH 283/671] add import --- nbconvert/exporters/pdf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nbconvert/exporters/pdf.py b/nbconvert/exporters/pdf.py index f66d685df..5e34238aa 100644 --- a/nbconvert/exporters/pdf.py +++ b/nbconvert/exporters/pdf.py @@ -8,7 +8,7 @@ import sys from ipython_genutils.py3compat import which, cast_bytes_py2, getcwd -from traitlets import Integer, List, Bool, Instance, Unicode +from traitlets import Integer, List, Bool, Instance, Unicode, default from testpath.tempdir import TemporaryWorkingDirectory from .latex import LatexExporter From a91633829eec6e648fbb6f86271ab8c39a486857 Mon Sep 17 00:00:00 2001 From: Tyler Makaro Date: Wed, 24 Apr 2019 21:04:48 -0700 Subject: [PATCH 284/671] make sure intermediate file is saved as a .tex file --- nbconvert/exporters/pdf.py | 1 + 1 file changed, 1 insertion(+) diff --git a/nbconvert/exporters/pdf.py b/nbconvert/exporters/pdf.py index 5e34238aa..08fc139c7 100644 --- a/nbconvert/exporters/pdf.py +++ b/nbconvert/exporters/pdf.py @@ -172,6 +172,7 @@ def from_notebook_node(self, nb, resources=None, **kw): self._captured_outputs = [] with TemporaryWorkingDirectory(): notebook_name = 'notebook' + resources['output_extension'] = '.tex' tex_file = self.writer.write(latex, resources, notebook_name=notebook_name) self.log.info("Building PDF") rc = self.run_latex(tex_file) From ca30458484f0bf8f474d8f865f587c7aa7eb6100 Mon Sep 17 00:00:00 2001 From: Tyler Makaro Date: Thu, 25 Apr 2019 08:28:59 -0700 Subject: [PATCH 285/671] fix export_form_notebook --- nbconvert/exporters/script.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nbconvert/exporters/script.py b/nbconvert/exporters/script.py index 5c4339857..7ebc60bda 100644 --- a/nbconvert/exporters/script.py +++ b/nbconvert/exporters/script.py @@ -14,7 +14,7 @@ class ScriptExporter(TemplateExporter): # Caches of already looked-up and instantiated exporters for delegation: _exporters = Dict() _lang_exporters = Dict() - export_form_notebook = "script" + export_from_notebook = "script" @default('template_file') def _template_file_default(self): From 3c9d9141472d0839d307c38cceb10716e1dd3bcb Mon Sep 17 00:00:00 2001 From: Matthew Seal Date: Thu, 25 Apr 2019 10:05:18 -0700 Subject: [PATCH 286/671] release 5.5 --- CONTRIBUTING.md | 2 + docs/source/changelog.rst | 117 ++++++++++++++++++++++++++++ docs/source/development_release.rst | 2 +- docs/source/execute_api.rst | 6 +- nbconvert/_version.py | 4 +- 5 files changed, 125 insertions(+), 6 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 99f7fd9ee..206978578 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -30,6 +30,8 @@ If you want to build the docs you will need to install the docs dependencies in the standard dependencies. You can get all of the dependencies by running `pip install -e .[all]` and if you want only those needed to run the docs you can access them with `pip install -e .[docs]`. +Full build instructions can be found at [docs/README.md](docs/README.md). + # Releasing If you are going to release a version of `nbconvert` you should also be capable diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst index b992dbabc..fb65120d7 100644 --- a/docs/source/changelog.rst +++ b/docs/source/changelog.rst @@ -4,6 +4,123 @@ Changes in nbconvert ==================== +5.5 +--- + +The following 18 authors contributed 144 commits -- Thank you all! + +* Benjamin Ragan-Kelley +* Clayton A Davis +* DInne Bosman +* Doug Blank +* Henrique Silva +* Jeff Hale +* Lukasz Mitusinski +* M Pacer +* Maarten Breddels +* Madhumitha N +* Matthew Seal +* Paul Gowder +* Philipp A +* Rick Lupton +* Rüdiger Busche +* Thomas Kluyver +* Tyler Makaro +* WrRan + +The full list of changes they made can be seen `on GitHub `__ + +Significant Changes +~~~~~~~~~~~~~~~~~~~ + +Deprecations +++++++++++++ + +Python 3.4 support was dropped. Many of our upstream libraries stopped supporting 3.4 and it was found that serious bugs were being caught during testing against those libraries updating past 3.4. + +See :ghpull:`979` for details. + +IPyWidget Support ++++++++++++++++++ + +Now when a notebook executing contains `Jupyter Widgets `__, the state of all the widgets can be stored in the notebook's metadata. This allows rendering of the live widgets on, for instance nbviewer, or when converting to html. + +You can tell nbconvert to not store the state using the `store_widget_state` argument:: + + jupyter nbconvert --ExecutePreprocessor.store_widget_state=False --to notebook --execute mynotebook.ipynb + +This widget rendering is not performed against a browser during execution, so only widget default states or states manipulated via user code will be calculated during execution. `%%javascript` cells will execute upon notebook rendering, enabling complex interactions to function as expected when viewed by a UI. + +If you can't view widget results after execution, you may need to select `Trust Notebook` under the `File` menu of the UI in question. + +See :ghpull:`779`, :ghpull:`900`, and :ghpull:`983` for details. + +Execute Preprocessor Rework ++++++++++++++++++++++++++++ + +Based on monkey patching required in `papermill `__ the `run_cell` code path in the ExecutePreprocessor was reworked to allow for accessing individual message parses without reimplementing the entire function. Now there is a `processs_message` function which take a ZeroMQ message and applies all of its side-effect updates on the cell/notebook objects before returning the output it generated, if it generated any such output. + +The change required a much more extensive test suite covering cell execution as test coverage on the various, sometimes wonky, code paths made improvements and reworks impossible to prove undamaging. Now changes to kernel message processing has much better coverage, so future additions or changes with specs over time will be easier to add. + +See :ghpull:`905` and :ghpull:`982` for details + +Out Of Memory Kernel Failure Catches +++++++++++++++++++++++++++++++++++++ + +When running out of memory on a machine, if the kernel process was killed by the operating system it would result in a timeout error at best and hang indefinitely at worst. Now regardless of timeout configuration, if the underlying kernel process dies before emitting any messages to the effect an exception will be raised notifying the consumer of the lost kernel within a few seconds. + +See :ghpull:`959`, :ghpull:`971`, and :ghpull:`998` for details + +Latex / PDF Template Improvements ++++++++++++++++++++++++++++++++++ + +The latex template was long overdue for improvements. The default template had a rewrite which makes exports for latex and pdf look a lot better. Code cells in particular render much better with line breaks and styling the more closely matches notebook browser rendering. Thanks t-makaro for the efforts here! + +See :ghpull:`992` for details + +Comprehensive notes +~~~~~~~~~~~~~~~~~~~ + +New Features +++++++++++++ +- IPyWidget Support :ghpull:`779`, :ghpull:`900`, and :ghpull:`983` +- A new ClearMetadata Preprocessor is available :ghpull:`805`: +- Support for pandoc 2 :ghpull:`964`: +- New, and better, latex template :ghpull:`992`: + +Fixing Problems ++++++++++++++++ +- Refactored execute preprocessor to have a process_message function :ghpull:`905`: +- Fixed OOM kernel failures hanging :ghpull:`959` and :ghpull:`971`: +- Fixed latex export for svg data in python 3 :ghpull:`985`: +- Enabled configuration to be shared to exporters from script exporter :ghpull:`993`: +- Make latex errors less verbose :ghpull:`988`: +- Typo in template syntax :ghpull:`984`: +- Improved attachments +fix supporting non-unique names :ghpull:`980`: +- PDFExporter "output_mimetype" traitlet is not longer 'text/latex' :ghpull:`972`: +- FIX: respect wait for clear_output :ghpull:`969`: +- address deprecation warning in cgi.escape :ghpull:`963`: +- Correct inaccurate description of available LaTeX template :ghpull:`958`: +- Fixed kernel death detection for executions with timeouts :ghpull:`998`: +- Fixed export names for various templates :ghpull:`1000`, :ghpull:`1001`, and :ghpull:`1001`: + +Deprecations +++++++++++++ +- Dropped support for python 3.4 :ghpull:`979`: +- Removed deprecated ``export_by_name`` :ghpull:`945`: + +Testing, Docs, and Builds ++++++++++++++++++++++++++ +- Added tests for each branch in execute's run_cell method :ghpull:`982`: +- Mention formats in --to options more clearly :ghpull:`991`: +- Adds ascii output type to command line docs page, mention image folder output :ghpull:`956`: +- Simplify setup.py :ghpull:`949`: +- Use utf-8 encoding in execute_api example :ghpull:`921`: +- Upgrade pytest on Travis :ghpull:`941`: +- Fix LaTeX base template name in docs :ghpull:`940`: +- Updated release instructions based on 5.4 release walk-through :ghpull:`887`: +- Fixed broken link to jinja docs :ghpull:`997`: + 5.4.1 ----- `5.4.1 on Github `__ diff --git a/docs/source/development_release.rst b/docs/source/development_release.rst index e56fc6ce9..73b9feb4a 100644 --- a/docs/source/development_release.rst +++ b/docs/source/development_release.rst @@ -70,7 +70,7 @@ Create the release #. Update the :doc:`changelog ` to account for all the PRs assigned to this milestone. -#. Update version number in ``notebook/_version.py``. +#. Update version number in ``notebook/_version.py`` and remove ``.dev`` from dev_info. #. Commit and tag the release with the current version number: diff --git a/docs/source/execute_api.rst b/docs/source/execute_api.rst index a10b2a39d..1a9da82b1 100644 --- a/docs/source/execute_api.rst +++ b/docs/source/execute_api.rst @@ -166,9 +166,9 @@ Widget state If your notebook contains any `Jupyter Widgets `_, -the state of all the widgets can be stored in the notebook's metadata (starting -from nbconvert 5.5). This allows rendering of the live widgets on for instance -nbviewer, or when converting to html. +the state of all the widgets can be stored in the notebook's metadata. +This allows rendering of the live widgets on for instance nbviewer, or when +converting to html. We can tell nbconvert to not store the state using the `store_widget_state` argument:: diff --git a/nbconvert/_version.py b/nbconvert/_version.py index 6091cd783..cce58a98c 100644 --- a/nbconvert/_version.py +++ b/nbconvert/_version.py @@ -1,6 +1,6 @@ -version_info = (5, 4, 1) +version_info = (5, 5, 0) pre_info = '' -dev_info = '.dev' +dev_info = '' def create_valid_version(release_info, epoch=None, pre_input='', dev_input=''): ''' From 48015d036da9bc2b09eb6bcd45d28b1262c8e6a7 Mon Sep 17 00:00:00 2001 From: Matthew Seal Date: Thu, 25 Apr 2019 10:25:47 -0700 Subject: [PATCH 287/671] Added .dev back to version --- nbconvert/_version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nbconvert/_version.py b/nbconvert/_version.py index cce58a98c..a8d789249 100644 --- a/nbconvert/_version.py +++ b/nbconvert/_version.py @@ -1,6 +1,6 @@ version_info = (5, 5, 0) pre_info = '' -dev_info = '' +dev_info = '.dev' def create_valid_version(release_info, epoch=None, pre_input='', dev_input=''): ''' From a266caa1a1340c2de4c4b11861fce5f139b2ede4 Mon Sep 17 00:00:00 2001 From: Dustin H Date: Tue, 30 Apr 2019 16:44:53 -0400 Subject: [PATCH 288/671] use check_alive in poll --- nbconvert/preprocessors/execute.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/nbconvert/preprocessors/execute.py b/nbconvert/preprocessors/execute.py index 2c65b8a8a..f5daeb7e6 100644 --- a/nbconvert/preprocessors/execute.py +++ b/nbconvert/preprocessors/execute.py @@ -460,10 +460,7 @@ def _poll_for_reply(self, msg_id, cell=None, timeout=None): return msg except Empty: # received no message, check if kernel is still alive - if not self.kc.is_alive(): - self.log.error( - "Kernel died while waiting for execute reply.") - raise RuntimeError("Kernel died") + self._check_alive() # kernel still alive, wait for a message def _get_timeout(self, cell): From 05f1e4812cb6b11cb174bc7aab50252c76968420 Mon Sep 17 00:00:00 2001 From: Valery M Date: Wed, 1 May 2019 18:15:21 -0400 Subject: [PATCH 289/671] Couldn't pass by --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 218f2a64f..150266e85 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,7 @@ filename of the Jupyter notebook. ### Example: Convert a notebook to HTML -Convert Juptyer notebook file, `mynotebook.ipynb`, to HTML using: +Convert Jupyter notebook file, `mynotebook.ipynb`, to HTML using: $ jupyter nbconvert --to html mynotebook.ipynb From da1ad6b5496992ad42bf5fead7e7c9b03207ffd3 Mon Sep 17 00:00:00 2001 From: Sylvain Corlay Date: Thu, 2 May 2019 00:40:33 +0200 Subject: [PATCH 290/671] Remove un-necessary css --- nbconvert/preprocessors/csshtmlheader.py | 41 ------------------------ 1 file changed, 41 deletions(-) diff --git a/nbconvert/preprocessors/csshtmlheader.py b/nbconvert/preprocessors/csshtmlheader.py index 4feff8fed..191a2ea6c 100755 --- a/nbconvert/preprocessors/csshtmlheader.py +++ b/nbconvert/preprocessors/csshtmlheader.py @@ -78,47 +78,6 @@ def _generate_header(self, resources): pygments_css = formatter.get_style_defs(self.highlight_class) header.append(pygments_css) - # These ANSI CSS definitions will be part of style.min.css with the - # Notebook release 5.0 and shall be removed afterwards! - # See https://github.com/jupyter/nbconvert/pull/259 - header.append(""" -/* Temporary definitions which will become obsolete with Notebook release 5.0 */ -.ansi-black-fg { color: #3E424D; } -.ansi-black-bg { background-color: #3E424D; } -.ansi-black-intense-fg { color: #282C36; } -.ansi-black-intense-bg { background-color: #282C36; } -.ansi-red-fg { color: #E75C58; } -.ansi-red-bg { background-color: #E75C58; } -.ansi-red-intense-fg { color: #B22B31; } -.ansi-red-intense-bg { background-color: #B22B31; } -.ansi-green-fg { color: #00A250; } -.ansi-green-bg { background-color: #00A250; } -.ansi-green-intense-fg { color: #007427; } -.ansi-green-intense-bg { background-color: #007427; } -.ansi-yellow-fg { color: #DDB62B; } -.ansi-yellow-bg { background-color: #DDB62B; } -.ansi-yellow-intense-fg { color: #B27D12; } -.ansi-yellow-intense-bg { background-color: #B27D12; } -.ansi-blue-fg { color: #208FFB; } -.ansi-blue-bg { background-color: #208FFB; } -.ansi-blue-intense-fg { color: #0065CA; } -.ansi-blue-intense-bg { background-color: #0065CA; } -.ansi-magenta-fg { color: #D160C4; } -.ansi-magenta-bg { background-color: #D160C4; } -.ansi-magenta-intense-fg { color: #A03196; } -.ansi-magenta-intense-bg { background-color: #A03196; } -.ansi-cyan-fg { color: #60C6C8; } -.ansi-cyan-bg { background-color: #60C6C8; } -.ansi-cyan-intense-fg { color: #258F8F; } -.ansi-cyan-intense-bg { background-color: #258F8F; } -.ansi-white-fg { color: #C5C1B4; } -.ansi-white-bg { background-color: #C5C1B4; } -.ansi-white-intense-fg { color: #A1A6B2; } -.ansi-white-intense-bg { background-color: #A1A6B2; } - -.ansi-bold { font-weight: bold; } -""") - # Load the user's custom CSS and IPython's default custom CSS. If they # differ, assume the user has made modifications to his/her custom CSS # and that we should inline it in the nbconvert output. From 80d55cd4c72cfade1cd493e4b5bb3e8630ee24a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miro=20Hron=C4=8Dok?= Date: Thu, 2 May 2019 11:57:11 +0200 Subject: [PATCH 291/671] Require mock for tests ______ ERROR collecting nbconvert/preprocessors/tests/test_execute.py ______ ImportError while importing test module '.../jupyter/nbconvert/nbconvert/preprocessors/tests/test_execute.py'. Hint: make sure your test modules/packages have valid Python names. Traceback: nbconvert/preprocessors/tests/test_execute.py:26: in from mock import MagicMock E ModuleNotFoundError: No module named 'mock' --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 9b115596b..deca85375 100644 --- a/setup.py +++ b/setup.py @@ -212,7 +212,7 @@ def run(self): jupyter_client_req = 'jupyter_client>=4.2' extra_requirements = { - 'test': ['pytest', 'pytest-cov', 'ipykernel', jupyter_client_req, 'ipywidgets>=7'], + 'test': ['pytest', 'pytest-cov', 'mock', 'ipykernel', jupyter_client_req, 'ipywidgets>=7'], 'serve': ['tornado>=4.0'], 'execute': [jupyter_client_req], 'docs': ['sphinx>=1.5.1', From e76ad5060637a92f0870a3e508c62af9b557af7e Mon Sep 17 00:00:00 2001 From: Dustin H Date: Thu, 2 May 2019 09:52:19 -0400 Subject: [PATCH 292/671] remove unneeded return --- nbconvert/preprocessors/execute.py | 1 - 1 file changed, 1 deletion(-) diff --git a/nbconvert/preprocessors/execute.py b/nbconvert/preprocessors/execute.py index f5daeb7e6..14b47b612 100644 --- a/nbconvert/preprocessors/execute.py +++ b/nbconvert/preprocessors/execute.py @@ -480,7 +480,6 @@ def _handle_timeout(self): if self.interrupt_on_timeout: self.log.error("Interrupting kernel") self.km.interrupt_kernel() - return else: raise TimeoutError("Cell execution timed out") From c08e527a8a7dbe20bdd195db6db966ee1671460e Mon Sep 17 00:00:00 2001 From: Dustin H Date: Thu, 2 May 2019 16:17:46 -0400 Subject: [PATCH 293/671] add a couple deadline tests --- nbconvert/preprocessors/execute.py | 5 ++-- nbconvert/preprocessors/tests/test_execute.py | 26 +++++++++++++++++++ 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/nbconvert/preprocessors/execute.py b/nbconvert/preprocessors/execute.py index 14b47b612..9f34807ba 100644 --- a/nbconvert/preprocessors/execute.py +++ b/nbconvert/preprocessors/execute.py @@ -7,9 +7,9 @@ from textwrap import dedent from contextlib import contextmanager try: - from time import monotonic + from time import monotonic # Py 3 except ImportError: - from time import time as monotonic + from time import time as monotonic # Py 2 try: from queue import Empty # Py 3 @@ -526,6 +526,7 @@ def run_cell(self, cell, cell_index=0): parent_msg_id = self.kc.execute(cell.source) self.log.debug("Executing cell:\n%s", cell.source) exec_timeout = self._get_timeout(cell) + deadline = None if exec_timeout is not None: deadline = monotonic() + exec_timeout diff --git a/nbconvert/preprocessors/tests/test_execute.py b/nbconvert/preprocessors/tests/test_execute.py index f441e0511..53df06257 100644 --- a/nbconvert/preprocessors/tests/test_execute.py +++ b/nbconvert/preprocessors/tests/test_execute.py @@ -31,6 +31,10 @@ from testpath import modified_env from ipython_genutils.py3compat import string_types +try: + from queue import Queue # Py 3 +except ImportError: + from Queue import Queue # Py2 try: TimeoutError # Py 3 except NameError: @@ -507,6 +511,28 @@ def test_busy_message(self, preprocessor, cell_mock, message_mock): # Ensure no outputs were generated assert cell_mock.outputs == [] + @ExecuteTestBase.prepare_cell_mocks() + def test_deadline_exec_reply(self, preprocessor, cell_mock, message_mock): + q = Queue() + # Both channels will never receive, so we expect to hit the timeout. + preprocessor.kc.shell_channel.get_msg = lambda timeout=0 : q.get(timeout=timeout) + preprocessor.kc.iopub_channel.get_msg = lambda timeout=0 : q.get(timeout=timeout) + preprocessor.timeout = 1 + + with pytest.raises(TimeoutError): + preprocessor.run_cell(cell_mock) + + @ExecuteTestBase.prepare_cell_mocks() + def test_deadline_iopub(self, preprocessor, cell_mock, message_mock): + q = Queue() + # The shell_channel will complete, so we expect only to hit the iopub timeout. + preprocessor.kc.iopub_channel.get_msg = lambda timeout=0 : q.get(timeout=timeout) + preprocessor.iopub_timeout = 1 + preprocessor.raise_on_iopub_timeout = True + + with pytest.raises(RuntimeError): + preprocessor.run_cell(cell_mock) + @ExecuteTestBase.prepare_cell_mocks({ 'msg_type': 'execute_input', 'header': {'msg_type': 'execute_input'}, From b959e189cde06f41d30ea933f6bacbbd28750e0d Mon Sep 17 00:00:00 2001 From: Sylvain Corlay Date: Thu, 2 May 2019 23:21:09 +0200 Subject: [PATCH 294/671] Close image tag --- nbconvert/templates/html/basic.tpl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nbconvert/templates/html/basic.tpl b/nbconvert/templates/html/basic.tpl index 9159415ab..7d6767412 100644 --- a/nbconvert/templates/html/basic.tpl +++ b/nbconvert/templates/html/basic.tpl @@ -118,7 +118,7 @@ unknown type {{ cell.type }} {% block data_svg scoped -%}
{%- if output.svg_filename %} - {%- else %} {{ output.data['image/svg+xml'] }} {%- endif %} From f8c48013f5c7acb76a97921e01c6e2ad9984e961 Mon Sep 17 00:00:00 2001 From: Matthew Seal Date: Sun, 5 May 2019 11:48:46 -0400 Subject: [PATCH 295/671] Upgraded timeout tests for run_cell improvements --- nbconvert/preprocessors/execute.py | 7 +- nbconvert/preprocessors/tests/test_execute.py | 64 +++++++++++++++---- 2 files changed, 54 insertions(+), 17 deletions(-) diff --git a/nbconvert/preprocessors/execute.py b/nbconvert/preprocessors/execute.py index 9f34807ba..f115afac9 100644 --- a/nbconvert/preprocessors/execute.py +++ b/nbconvert/preprocessors/execute.py @@ -537,7 +537,6 @@ def run_cell(self, cell, cell_index=0): polling_exec_reply = True while more_output or polling_exec_reply: - if polling_exec_reply: if self._passed_deadline(deadline): polling_exec_reply = False @@ -559,7 +558,7 @@ def run_cell(self, cell, cell_index=0): continue if self.raise_on_iopub_timeout: - raise RuntimeError("Timeout waiting for IOPub output") + raise TimeoutError("Timeout waiting for IOPub output") else: self.log.warning("Timeout waiting for IOPub output") more_output = False @@ -568,11 +567,11 @@ def run_cell(self, cell, cell_index=0): # not an output from our execution continue - # Will raise CellExecutionComplete when completed try: + # Will raise CellExecutionComplete when completed self.process_message(msg, cell, cell_index) except CellExecutionComplete: - break + more_output = False # Return cell.outputs still for backwards compatability return exec_reply, cell.outputs diff --git a/nbconvert/preprocessors/tests/test_execute.py b/nbconvert/preprocessors/tests/test_execute.py index 53df06257..ef49a26bd 100644 --- a/nbconvert/preprocessors/tests/test_execute.py +++ b/nbconvert/preprocessors/tests/test_execute.py @@ -32,9 +32,9 @@ from ipython_genutils.py3compat import string_types try: - from queue import Queue # Py 3 + from queue import Empty # Py 3 except ImportError: - from Queue import Queue # Py2 + from Queue import Empty # Py 2 try: TimeoutError # Py 3 except NameError: @@ -146,6 +146,7 @@ def test_mock_wrapper(self): shell_channel=MagicMock(get_msg=shell_channel_message_mock()), execute=MagicMock(return_value=parent_id) ) + preprocessor.parent_id = parent_id return func(self, preprocessor, cell_mock, message_mock) return test_mock_wrapper return prepared_wrapper @@ -342,12 +343,12 @@ def test_kernel_death(self): except TimeoutError: pass km, kc = preprocessor.start_new_kernel() - + with patch.object(km, "is_alive") as alive_mock: alive_mock.return_value = False with pytest.raises(DeadKernelError): input_nb, output_nb = preprocessor.preprocess(input_nb, {}, km=km) - + def test_allow_errors(self): """ @@ -511,28 +512,65 @@ def test_busy_message(self, preprocessor, cell_mock, message_mock): # Ensure no outputs were generated assert cell_mock.outputs == [] - @ExecuteTestBase.prepare_cell_mocks() + @ExecuteTestBase.prepare_cell_mocks({ + 'msg_type': 'stream', + 'header': {'msg_type': 'stream'}, + 'content': {'name': 'stdout', 'text': 'foo'}, + }, { + 'msg_type': 'stream', + 'header': {'msg_type': 'stream'}, + 'content': {'name': 'stderr', 'text': 'bar'} + }) def test_deadline_exec_reply(self, preprocessor, cell_mock, message_mock): - q = Queue() - # Both channels will never receive, so we expect to hit the timeout. - preprocessor.kc.shell_channel.get_msg = lambda timeout=0 : q.get(timeout=timeout) - preprocessor.kc.iopub_channel.get_msg = lambda timeout=0 : q.get(timeout=timeout) + # exec_reply is never received, so we expect to hit the timeout. + preprocessor.kc.shell_channel.get_msg = MagicMock(side_effect=Empty()) preprocessor.timeout = 1 with pytest.raises(TimeoutError): preprocessor.run_cell(cell_mock) + assert message_mock.call_count == 3 + # Ensure the output was captured + self.assertListEqual(cell_mock.outputs, [ + {'output_type': 'stream', 'name': 'stdout', 'text': 'foo'}, + {'output_type': 'stream', 'name': 'stderr', 'text': 'bar'} + ]) + @ExecuteTestBase.prepare_cell_mocks() def test_deadline_iopub(self, preprocessor, cell_mock, message_mock): - q = Queue() # The shell_channel will complete, so we expect only to hit the iopub timeout. - preprocessor.kc.iopub_channel.get_msg = lambda timeout=0 : q.get(timeout=timeout) - preprocessor.iopub_timeout = 1 + message_mock.side_effect = Empty() preprocessor.raise_on_iopub_timeout = True - with pytest.raises(RuntimeError): + with pytest.raises(TimeoutError): preprocessor.run_cell(cell_mock) + @ExecuteTestBase.prepare_cell_mocks({ + 'msg_type': 'stream', + 'header': {'msg_type': 'stream'}, + 'content': {'name': 'stdout', 'text': 'foo'}, + }, { + 'msg_type': 'stream', + 'header': {'msg_type': 'stream'}, + 'content': {'name': 'stderr', 'text': 'bar'} + }) + def test_eventual_deadline_iopub(self, preprocessor, cell_mock, message_mock): + # Process a few messages before raising a timeout from iopub + message_mock.side_effect = list(message_mock.side_effect)[:-1] + [Empty()] + preprocessor.kc.shell_channel.get_msg = MagicMock( + return_value={'parent_header': {'msg_id': preprocessor.parent_id}}) + preprocessor.raise_on_iopub_timeout = True + + with pytest.raises(TimeoutError): + preprocessor.run_cell(cell_mock) + + assert message_mock.call_count == 3 + # Ensure the output was captured + self.assertListEqual(cell_mock.outputs, [ + {'output_type': 'stream', 'name': 'stdout', 'text': 'foo'}, + {'output_type': 'stream', 'name': 'stderr', 'text': 'bar'} + ]) + @ExecuteTestBase.prepare_cell_mocks({ 'msg_type': 'execute_input', 'header': {'msg_type': 'execute_input'}, From 5a124d9940494eb6006b3f9f5f397c938218552b Mon Sep 17 00:00:00 2001 From: Matthew Seal Date: Sun, 5 May 2019 14:20:41 -0400 Subject: [PATCH 296/671] Removed newlines from clear notebook test. Refactored test_run_notebooks --- .../tests/files/Clear Output.ipynb | 16 +- nbconvert/preprocessors/tests/test_execute.py | 457 +++++++++--------- 2 files changed, 237 insertions(+), 236 deletions(-) diff --git a/nbconvert/preprocessors/tests/files/Clear Output.ipynb b/nbconvert/preprocessors/tests/files/Clear Output.ipynb index 91c16a593..45328e99c 100644 --- a/nbconvert/preprocessors/tests/files/Clear Output.ipynb +++ b/nbconvert/preprocessors/tests/files/Clear Output.ipynb @@ -51,12 +51,12 @@ "name": "stdout", "output_type": "stream", "text": [ - "Hello world\n" + "Hello world" ] } ], "source": [ - "print(\"Hello world\")\n", + "print(\"Hello world\", end='')\n", "clear_output(wait=True) # no output after this" ] }, @@ -69,14 +69,14 @@ "name": "stdout", "output_type": "stream", "text": [ - "world\n" + "world" ] } ], "source": [ - "print(\"Hello\")\n", + "print(\"Hello\", end='')\n", "clear_output(wait=True) # here we have new output after wait=True\n", - "print(\"world\")" + "print(\"world\", end='')" ] }, { @@ -165,14 +165,14 @@ "name": "stdout", "output_type": "stream", "text": [ - "world\n" + "world" ] } ], "source": [ "handle4 = display(\"Hello\", display_id=\"id4\")\n", "clear_output(wait=True)\n", - "print('world')" + "print('world', end='')" ] }, { @@ -188,4 +188,4 @@ "metadata": {}, "nbformat": 4, "nbformat_minor": 0 -} \ No newline at end of file +} diff --git a/nbconvert/preprocessors/tests/test_execute.py b/nbconvert/preprocessors/tests/test_execute.py index f441e0511..9d6e26b7b 100644 --- a/nbconvert/preprocessors/tests/test_execute.py +++ b/nbconvert/preprocessors/tests/test_execute.py @@ -43,8 +43,10 @@ addr_pat = re.compile(r'0x[0-9a-f]{7,9}') ipython_input_pat = re.compile(r'') current_dir = os.path.dirname(__file__) +IPY_MAJOR = IPython.version_info[0] -def _normalize_base64(b64_text): + +def normalize_base64(b64_text): # if it's base64, pass it through b64 decode/encode to avoid # equivalent values from being considered unequal try: @@ -52,209 +54,208 @@ def _normalize_base64(b64_text): except (ValueError, TypeError): return b64_text -class ExecuteTestBase(PreprocessorTestsBase): - def build_preprocessor(self, opts): - """Make an instance of a preprocessor""" - preprocessor = ExecutePreprocessor() - preprocessor.enabled = True - for opt in opts: - setattr(preprocessor, opt, opts[opt]) - # Perform some state setup that should probably be in the init - preprocessor._display_id_map = {} - preprocessor.widget_state = {} - preprocessor.widget_buffers = {} - return preprocessor - - @staticmethod - def prepare_cell_mocks(*messages): - """ - This function prepares a preprocessor object which has a fake kernel client - to mock the messages sent over zeromq. The mock kernel client will return - the messages passed into this wrapper back from `preproc.kc.iopub_channel.get_msg` - callbacks. It also appends a kernel idle message to the end of messages. - This allows for testing in with following call expectations: +def build_preprocessor(opts): + """Make an instance of a preprocessor""" + preprocessor = ExecutePreprocessor() + preprocessor.enabled = True + for opt in opts: + setattr(preprocessor, opt, opts[opt]) + # Perform some state setup that should probably be in the init + preprocessor._display_id_map = {} + preprocessor.widget_state = {} + preprocessor.widget_buffers = {} + return preprocessor - @ExecuteTestBase.prepare_cell_mocks({ - 'msg_type': 'stream', - 'header': {'msg_type': 'stream'}, - 'content': {'name': 'stdout', 'text': 'foo'}, - }) - def test_message_foo(self, preprocessor, cell_mock, message_mock): - preprocessor.kc.iopub_channel.get_msg() - # => - # { - # 'msg_type': 'stream', - # 'parent_header': {'msg_id': 'fake_id'}, - # 'header': {'msg_type': 'stream'}, - # 'content': {'name': 'stdout', 'text': 'foo'}, - # } - preprocessor.kc.iopub_channel.get_msg() - # => - # { - # 'msg_type': 'status', - # 'parent_header': {'msg_id': 'fake_id'}, - # 'content': {'execution_state': 'idle'}, - # } - preprocessor.kc.iopub_channel.get_msg() # => None - message_mock.call_count # => 3 - """ - parent_id = 'fake_id' - messages = list(messages) - # Always terminate messages with an idle to exit the loop - messages.append({'msg_type': 'status', 'content': {'execution_state': 'idle'}}) - - def shell_channel_message_mock(): - # Return the message generator for - # self.kc.shell_channel.get_msg => {'parent_header': {'msg_id': parent_id}} - return MagicMock(return_value={'parent_header': {'msg_id': parent_id}}) - - def iopub_messages_mock(): - # Return the message generator for - # self.kc.iopub_channel.get_msg => messages[i] - return MagicMock( - side_effect=[ - # Default the parent_header so mocks don't need to include this - ExecuteTestBase.merge_dicts( - {'parent_header': {'msg_id': parent_id}}, msg) - for msg in messages - ] - ) - def prepared_wrapper(func): - @functools.wraps(func) - def test_mock_wrapper(self): - """ - This inner function wrapper populates the preprocessor object with - the fake kernel client. This client has it's iopub and shell - channels mocked so as to fake the setup handshake and return - the messages passed into prepare_cell_mocks as the run_cell loop - processes them. - """ - cell_mock = NotebookNode(source='"foo" = "bar"', outputs=[]) - preprocessor = self.build_preprocessor({}) - preprocessor.nb = {'cells': [cell_mock]} - - # self.kc.iopub_channel.get_msg => message_mock.side_effect[i] - message_mock = iopub_messages_mock() - preprocessor.kc = MagicMock( - iopub_channel=MagicMock(get_msg=message_mock), - shell_channel=MagicMock(get_msg=shell_channel_message_mock()), - execute=MagicMock(return_value=parent_id) - ) - return func(self, preprocessor, cell_mock, message_mock) - return test_mock_wrapper - return prepared_wrapper - - -class TestExecute(ExecuteTestBase): - """Contains test functions for execute.py""" - maxDiff = None +def run_notebook(filename, opts, resources): + """Loads and runs a notebook, returning both the version prior to + running it and the version after running it. - @staticmethod - def normalize_output(output): - """ - Normalizes outputs for comparison. - """ - output = dict(output) - if 'metadata' in output: - del output['metadata'] - if 'text' in output: - output['text'] = re.sub(addr_pat, '', output['text']) - if 'text/plain' in output.get('data', {}): - output['data']['text/plain'] = \ - re.sub(addr_pat, '', output['data']['text/plain']) - if 'application/vnd.jupyter.widget-view+json' in output.get('data', {}): - output['data']['application/vnd.jupyter.widget-view+json'] \ - ['model_id'] = '' - for key, value in output.get('data', {}).items(): - if isinstance(value, string_types): - if sys.version_info.major == 2: - value = value.replace('u\'', '\'') - output['data'][key] = _normalize_base64(value) - if 'traceback' in output: - tb = [ - re.sub(ipython_input_pat, '', strip_ansi(line)) - for line in output['traceback'] - ] - output['traceback'] = tb + """ + with io.open(filename) as f: + input_nb = nbformat.read(f, 4) - return output + preprocessor = build_preprocessor(opts) + cleaned_input_nb = copy.deepcopy(input_nb) + for cell in cleaned_input_nb.cells: + if 'execution_count' in cell: + del cell['execution_count'] + cell['outputs'] = [] + # Override terminal size to standardise traceback format + with modified_env({'COLUMNS': '80', 'LINES': '24'}): + output_nb, _ = preprocessor(cleaned_input_nb, resources) - def assert_notebooks_equal(self, expected, actual): - expected_cells = expected['cells'] - actual_cells = actual['cells'] - assert len(expected_cells) == len(actual_cells) + return input_nb, output_nb - for expected_cell, actual_cell in zip(expected_cells, actual_cells): - expected_outputs = expected_cell.get('outputs', []) - actual_outputs = actual_cell.get('outputs', []) - normalized_expected_outputs = list(map(self.normalize_output, expected_outputs)) - normalized_actual_outputs = list(map(self.normalize_output, actual_outputs)) - assert normalized_expected_outputs == normalized_actual_outputs - expected_execution_count = expected_cell.get('execution_count', None) - actual_execution_count = actual_cell.get('execution_count', None) - assert expected_execution_count == actual_execution_count +def prepare_cell_mocks(*messages): + """ + This function prepares a preprocessor object which has a fake kernel client + to mock the messages sent over zeromq. The mock kernel client will return + the messages passed into this wrapper back from `preproc.kc.iopub_channel.get_msg` + callbacks. It also appends a kernel idle message to the end of messages. + This allows for testing in with following call expectations: + + @prepare_cell_mocks({ + 'msg_type': 'stream', + 'header': {'msg_type': 'stream'}, + 'content': {'name': 'stdout', 'text': 'foo'}, + }) + def test_message_foo(self, preprocessor, cell_mock, message_mock): + preprocessor.kc.iopub_channel.get_msg() + # => + # { + # 'msg_type': 'stream', + # 'parent_header': {'msg_id': 'fake_id'}, + # 'header': {'msg_type': 'stream'}, + # 'content': {'name': 'stdout', 'text': 'foo'}, + # } + preprocessor.kc.iopub_channel.get_msg() + # => + # { + # 'msg_type': 'status', + # 'parent_header': {'msg_id': 'fake_id'}, + # 'content': {'execution_state': 'idle'}, + # } + preprocessor.kc.iopub_channel.get_msg() # => None + message_mock.call_count # => 3 + """ + parent_id = 'fake_id' + messages = list(messages) + # Always terminate messages with an idle to exit the loop + messages.append({'msg_type': 'status', 'content': {'execution_state': 'idle'}}) + + def shell_channel_message_mock(): + # Return the message generator for + # self.kc.shell_channel.get_msg => {'parent_header': {'msg_id': parent_id}} + return MagicMock(return_value={'parent_header': {'msg_id': parent_id}}) + + def iopub_messages_mock(): + # Return the message generator for + # self.kc.iopub_channel.get_msg => messages[i] + return MagicMock( + side_effect=[ + # Default the parent_header so mocks don't need to include this + ExecuteTestBase.merge_dicts( + {'parent_header': {'msg_id': parent_id}}, msg) + for msg in messages + ] + ) + + def prepared_wrapper(func): + @functools.wraps(func) + def test_mock_wrapper(self): + """ + This inner function wrapper populates the preprocessor object with + the fake kernel client. This client has it's iopub and shell + channels mocked so as to fake the setup handshake and return + the messages passed into prepare_cell_mocks as the run_cell loop + processes them. + """ + cell_mock = NotebookNode(source='"foo" = "bar"', outputs=[]) + preprocessor = self.build_preprocessor({}) + preprocessor.nb = {'cells': [cell_mock]} + + # self.kc.iopub_channel.get_msg => message_mock.side_effect[i] + message_mock = iopub_messages_mock() + preprocessor.kc = MagicMock( + iopub_channel=MagicMock(get_msg=message_mock), + shell_channel=MagicMock(get_msg=shell_channel_message_mock()), + execute=MagicMock(return_value=parent_id) + ) + return func(self, preprocessor, cell_mock, message_mock) + return test_mock_wrapper + return prepared_wrapper + + +def normalize_output(output): + """ + Normalizes outputs for comparison. + """ + output = dict(output) + if 'metadata' in output: + del output['metadata'] + if 'text' in output: + output['text'] = re.sub(addr_pat, '', output['text']) + if 'text/plain' in output.get('data', {}): + output['data']['text/plain'] = \ + re.sub(addr_pat, '', output['data']['text/plain']) + if 'application/vnd.jupyter.widget-view+json' in output.get('data', {}): + output['data']['application/vnd.jupyter.widget-view+json'] \ + ['model_id'] = '' + for key, value in output.get('data', {}).items(): + if isinstance(value, string_types): + if sys.version_info.major == 2: + value = value.replace('u\'', '\'') + output['data'][key] = normalize_base64(value) + if 'traceback' in output: + tb = [ + re.sub(ipython_input_pat, '', strip_ansi(line)) + for line in output['traceback'] + ] + output['traceback'] = tb + + return output + + +def assert_notebooks_equal(expected, actual): + expected_cells = expected['cells'] + actual_cells = actual['cells'] + assert len(expected_cells) == len(actual_cells) + + for expected_cell, actual_cell in zip(expected_cells, actual_cells): + expected_outputs = expected_cell.get('outputs', []) + actual_outputs = actual_cell.get('outputs', []) + normalized_expected_outputs = list(map(normalize_output, expected_outputs)) + normalized_actual_outputs = list(map(normalize_output, actual_outputs)) + assert normalized_expected_outputs == normalized_actual_outputs + + expected_execution_count = expected_cell.get('execution_count', None) + actual_execution_count = actual_cell.get('execution_count', None) + assert expected_execution_count == actual_execution_count + + +@pytest.mark.parametrize( + ["input_name", "opts"], + [ + ("Clear Output.ipynb", dict(kernel_name="python")), + ("Empty Cell.ipynb", dict(kernel_name="python")), + ("Factorials.ipynb", dict(kernel_name="python")), + ("HelloWorld.ipynb", dict(kernel_name="python")), + ("Inline Image.ipynb", dict(kernel_name="python")), + ("Interrupt-IPY6.ipynb", dict(kernel_name="python", timeout=1, interrupt_on_timeout=True, allow_errors=True)) if IPY_MAJOR < 7 else + ("Interrupt.ipynb", dict(kernel_name="python", timeout=1, interrupt_on_timeout=True, allow_errors=True)), + ("JupyterWidgets.ipynb", dict(kernel_name="python")), + ("Skip Exceptions with Cell Tags-IPY6.ipynb", dict(kernel_name="python")) if IPY_MAJOR < 7 else + ("Skip Exceptions with Cell Tags.ipynb", dict(kernel_name="python")), + ("Skip Exceptions-IPY6.ipynb", dict(kernel_name="python", allow_errors=True)) if IPY_MAJOR < 7 else + ("Skip Exceptions.ipynb", dict(kernel_name="python", allow_errors=True)), + ("SVG.ipynb", dict(kernel_name="python")), + ("Unicode.ipynb", dict(kernel_name="python")), + ("UnicodePy3.ipynb", dict(kernel_name="python")), + ("update-display-id.ipynb", dict(kernel_name="python")), + ] +) +def test_run_all_notebooks(input_name, opts): + """Runs a series of test notebooks and compares them to their actual output""" + input_file = os.path.join(current_dir, 'files', input_name) + res = PreprocessorTestsBase().build_resources() + res['metadata']['path'] = os.path.join(current_dir, 'files') + input_nb, output_nb = run_notebook(input_file, opts, res) + assert_notebooks_equal(input_nb, output_nb) + + +class TestExecute(PreprocessorTestsBase): + """Contains test functions for execute.py""" + maxDiff = None def test_constructor(self): """Can a ExecutePreprocessor be constructed?""" self.build_preprocessor({}) - - def run_notebook(self, filename, opts, resources): - """Loads and runs a notebook, returning both the version prior to - running it and the version after running it. - - """ - with io.open(filename) as f: - input_nb = nbformat.read(f, 4) - - preprocessor = self.build_preprocessor(opts) - cleaned_input_nb = copy.deepcopy(input_nb) - for cell in cleaned_input_nb.cells: - if 'execution_count' in cell: - del cell['execution_count'] - cell['outputs'] = [] - - # Override terminal size to standardise traceback format - with modified_env({'COLUMNS': '80', 'LINES': '24'}): - output_nb, _ = preprocessor(cleaned_input_nb, resources) - - return input_nb, output_nb - - def test_run_notebooks(self): - """Runs a series of test notebooks and compares them to their actual output""" - input_files = glob.glob(os.path.join(current_dir, 'files', '*.ipynb')) - shared_opts = dict(kernel_name="python") - for filename in input_files: - # There is some slight differences between the output in IPython 6 and IPython 7. - IPY_MAJOR = IPython.version_info[0] - if os.path.basename(filename).endswith("-IPY6.ipynb"): - print(filename, IPY_MAJOR) - if IPY_MAJOR >= 7: - continue - elif os.path.basename(filename) in ("Interrupt.ipynb", "Skip Exceptions with Cell Tags.ipynb", "Skip Exceptions.ipynb"): - if IPY_MAJOR < 7: - continue - - # Special arguments for the notebooks - if os.path.basename(filename) == "Disable Stdin.ipynb": - continue - elif os.path.basename(filename) in ("Interrupt.ipynb", "Interrupt-IPY6.ipynb"): - opts = dict(timeout=1, interrupt_on_timeout=True, allow_errors=True) - elif os.path.basename(filename) in ("Skip Exceptions.ipynb", "Skip Exceptions-IPY6.ipynb"): - opts = dict(allow_errors=True) - else: - opts = dict() - res = self.build_resources() - res['metadata']['path'] = os.path.dirname(filename) - opts.update(shared_opts) - input_nb, output_nb = self.run_notebook(filename, opts, res) - self.assert_notebooks_equal(input_nb, output_nb) - def test_populate_language_info(self): preprocessor = self.build_preprocessor(opts=dict(kernel_name="python")) nb = nbformat.v4.new_notebook() # Certainly has no language_info. @@ -266,8 +267,8 @@ def test_empty_path(self): filename = os.path.join(current_dir, 'files', 'HelloWorld.ipynb') res = self.build_resources() res['metadata']['path'] = '' - input_nb, output_nb = self.run_notebook(filename, {}, res) - self.assert_notebooks_equal(input_nb, output_nb) + input_nb, output_nb = run_notebook(filename, {}, res) + assert_notebooks_equal(input_nb, output_nb) @pytest.mark.xfail("python3" not in KernelSpecManager().find_kernel_specs(), reason="requires a python3 kernelspec") @@ -279,17 +280,17 @@ def test_empty_kernel_name(self): """ filename = os.path.join(current_dir, 'files', 'UnicodePy3.ipynb') res = self.build_resources() - input_nb, output_nb = self.run_notebook(filename, {"kernel_name": ""}, res) - self.assert_notebooks_equal(input_nb, output_nb) + input_nb, output_nb = run_notebook(filename, {"kernel_name": ""}, res) + assert_notebooks_equal(input_nb, output_nb) with pytest.raises(TraitError): - input_nb, output_nb = self.run_notebook(filename, {"kernel_name": None}, res) + input_nb, output_nb = run_notebook(filename, {"kernel_name": None}, res) def test_disable_stdin(self): """Test disabling standard input""" filename = os.path.join(current_dir, 'files', 'Disable Stdin.ipynb') res = self.build_resources() res['metadata']['path'] = os.path.dirname(filename) - input_nb, output_nb = self.run_notebook(filename, dict(allow_errors=True), res) + input_nb, output_nb = run_notebook(filename, dict(allow_errors=True), res) # We need to special-case this particular notebook, because the # traceback contains machine-specific stuff like where IPython @@ -309,7 +310,7 @@ def test_timeout(self): res['metadata']['path'] = os.path.dirname(filename) with pytest.raises(TimeoutError): - self.run_notebook(filename, dict(timeout=1), res) + run_notebook(filename, dict(timeout=1), res) def test_timeout_func(self): """Check that an error is raised when a computation times out""" @@ -321,7 +322,7 @@ def timeout_func(source): return 10 with pytest.raises(TimeoutError): - self.run_notebook(filename, dict(timeout_func=timeout_func), res) + run_notebook(filename, dict(timeout_func=timeout_func), res) def test_kernel_death(self): """Check that an error is raised when the kernel is_alive is false""" @@ -338,12 +339,12 @@ def test_kernel_death(self): except TimeoutError: pass km, kc = preprocessor.start_new_kernel() - + with patch.object(km, "is_alive") as alive_mock: alive_mock.return_value = False with pytest.raises(DeadKernelError): input_nb, output_nb = preprocessor.preprocess(input_nb, {}, km=km) - + def test_allow_errors(self): """ @@ -353,7 +354,7 @@ def test_allow_errors(self): res = self.build_resources() res['metadata']['path'] = os.path.dirname(filename) with pytest.raises(CellExecutionError) as exc: - self.run_notebook(filename, dict(allow_errors=False), res) + run_notebook(filename, dict(allow_errors=False), res) self.assertIsInstance(str(exc.value), str) if sys.version_info >= (3, 0): assert u"# üñîçø∂é" in str(exc.value) @@ -370,7 +371,7 @@ def test_force_raise_errors(self): res = self.build_resources() res['metadata']['path'] = os.path.dirname(filename) with pytest.raises(CellExecutionError) as exc: - self.run_notebook(filename, dict(force_raise_errors=True), res) + run_notebook(filename, dict(force_raise_errors=True), res) self.assertIsInstance(str(exc.value), str) if sys.version_info >= (3, 0): assert u"# üñîçø∂é" in str(exc.value) @@ -427,7 +428,7 @@ def process_message(self, msg, cell, cell_index): assert outputs == [ {'name': 'stdout', 'output_type': 'stream', 'text': 'Hello World\n'} ] - self.assert_notebooks_equal(original, executed) + assert_notebooks_equal(original, executed) def test_execute_function(self): # Test the executenb() convenience API @@ -438,7 +439,7 @@ def test_execute_function(self): original = copy.deepcopy(input_nb) executed = executenb(original, os.path.dirname(filename)) - self.assert_notebooks_equal(original, executed) + assert_notebooks_equal(original, executed) def test_widgets(self): """Runs a test notebook with widgets and checks the widget state is saved.""" @@ -446,7 +447,7 @@ def test_widgets(self): opts = dict(kernel_name="python") res = self.build_resources() res['metadata']['path'] = os.path.dirname(input_file) - input_nb, output_nb = self.run_notebook(input_file, opts, res) + input_nb, output_nb = run_notebook(input_file, opts, res) output_data = [ output.get('data', {}) @@ -471,10 +472,10 @@ def test_widgets(self): assert 'version_minor' in wdata -class TestRunCell(ExecuteTestBase): +class TestRunCell(PreprocessorTestsBase): """Contains test functions for ExecutePreprocessor.run_cell""" - @ExecuteTestBase.prepare_cell_mocks() + @prepare_cell_mocks() def test_idle_message(self, preprocessor, cell_mock, message_mock): preprocessor.run_cell(cell_mock) # Just the exit message should be fetched @@ -482,7 +483,7 @@ def test_idle_message(self, preprocessor, cell_mock, message_mock): # Ensure no outputs were generated assert cell_mock.outputs == [] - @ExecuteTestBase.prepare_cell_mocks({ + @prepare_cell_mocks({ 'msg_type': 'stream', 'header': {'msg_type': 'execute_reply'}, 'parent_header': {'msg_id': 'wrong_parent'}, @@ -495,7 +496,7 @@ def test_message_for_wrong_parent(self, preprocessor, cell_mock, message_mock): # Ensure no output was written assert cell_mock.outputs == [] - @ExecuteTestBase.prepare_cell_mocks({ + @prepare_cell_mocks({ 'msg_type': 'status', 'header': {'msg_type': 'status'}, 'content': {'execution_state': 'busy'} @@ -507,7 +508,7 @@ def test_busy_message(self, preprocessor, cell_mock, message_mock): # Ensure no outputs were generated assert cell_mock.outputs == [] - @ExecuteTestBase.prepare_cell_mocks({ + @prepare_cell_mocks({ 'msg_type': 'execute_input', 'header': {'msg_type': 'execute_input'}, 'content': {} @@ -519,7 +520,7 @@ def test_execute_input_message(self, preprocessor, cell_mock, message_mock): # Ensure no outputs were generated assert cell_mock.outputs == [] - @ExecuteTestBase.prepare_cell_mocks({ + @prepare_cell_mocks({ 'msg_type': 'stream', 'header': {'msg_type': 'stream'}, 'content': {'name': 'stdout', 'text': 'foo'}, @@ -538,7 +539,7 @@ def test_stream_messages(self, preprocessor, cell_mock, message_mock): {'output_type': 'stream', 'name': 'stderr', 'text': 'bar'} ]) - @ExecuteTestBase.prepare_cell_mocks({ + @prepare_cell_mocks({ 'msg_type': 'stream', 'header': {'msg_type': 'execute_reply'}, 'content': {'name': 'stdout', 'text': 'foo'} @@ -554,7 +555,7 @@ def test_clear_output_message(self, preprocessor, cell_mock, message_mock): # Ensure the output was cleared assert cell_mock.outputs == [] - @ExecuteTestBase.prepare_cell_mocks({ + @prepare_cell_mocks({ 'msg_type': 'stream', 'header': {'msg_type': 'stream'}, 'content': {'name': 'stdout', 'text': 'foo'} @@ -574,7 +575,7 @@ def test_clear_output_wait_message(self, preprocessor, cell_mock, message_mock): {'output_type': 'stream', 'name': 'stdout', 'text': 'foo'} ] - @ExecuteTestBase.prepare_cell_mocks({ + @prepare_cell_mocks({ 'msg_type': 'stream', 'header': {'msg_type': 'stream'}, 'content': {'name': 'stdout', 'text': 'foo'} @@ -598,7 +599,7 @@ def test_clear_output_wait_then_message_message(self, preprocessor, cell_mock, m {'output_type': 'stream', 'name': 'stderr', 'text': 'bar'} ] - @ExecuteTestBase.prepare_cell_mocks({ + @prepare_cell_mocks({ 'msg_type': 'stream', 'header': {'msg_type': 'stream'}, 'content': {'name': 'stdout', 'text': 'foo'} @@ -622,7 +623,7 @@ def test_clear_output_wait_then_update_display_message(self, preprocessor, cell_ {'output_type': 'stream', 'name': 'stdout', 'text': 'foo'} ] - @ExecuteTestBase.prepare_cell_mocks({ + @prepare_cell_mocks({ 'msg_type': 'execute_reply', 'header': {'msg_type': 'execute_reply'}, 'content': {'execution_count': 42} @@ -635,7 +636,7 @@ def test_execution_count_message(self, preprocessor, cell_mock, message_mock): # Ensure no outputs were generated assert cell_mock.outputs == [] - @ExecuteTestBase.prepare_cell_mocks({ + @prepare_cell_mocks({ 'msg_type': 'stream', 'header': {'msg_type': 'stream'}, 'content': {'execution_count': 42, 'name': 'stdout', 'text': 'foo'} @@ -650,7 +651,7 @@ def test_execution_count_with_stream_message(self, preprocessor, cell_mock, mess {'output_type': 'stream', 'name': 'stdout', 'text': 'foo'} ] - @ExecuteTestBase.prepare_cell_mocks({ + @prepare_cell_mocks({ 'msg_type': 'comm', 'header': {'msg_type': 'comm'}, 'content': { @@ -668,7 +669,7 @@ def test_widget_comm_message(self, preprocessor, cell_mock, message_mock): # Ensure no outputs were generated assert cell_mock.outputs == [] - @ExecuteTestBase.prepare_cell_mocks({ + @prepare_cell_mocks({ 'msg_type': 'comm', 'header': {'msg_type': 'comm'}, 'buffers': [b'123'], @@ -691,7 +692,7 @@ def test_widget_comm_buffer_message(self, preprocessor, cell_mock, message_mock) # Ensure no outputs were generated assert cell_mock.outputs == [] - @ExecuteTestBase.prepare_cell_mocks({ + @prepare_cell_mocks({ 'msg_type': 'comm', 'header': {'msg_type': 'comm'}, 'content': { @@ -710,7 +711,7 @@ def test_unknown_comm_message(self, preprocessor, cell_mock, message_mock): # Ensure no outputs were generated assert cell_mock.outputs == [] - @ExecuteTestBase.prepare_cell_mocks({ + @prepare_cell_mocks({ 'msg_type': 'execute_result', 'header': {'msg_type': 'execute_result'}, 'content': { @@ -734,7 +735,7 @@ def test_execute_result_message(self, preprocessor, cell_mock, message_mock): # No display id was provided assert not preprocessor._display_id_map - @ExecuteTestBase.prepare_cell_mocks({ + @prepare_cell_mocks({ 'msg_type': 'execute_result', 'header': {'msg_type': 'execute_result'}, 'content': { @@ -758,7 +759,7 @@ def test_execute_result_with_display_message(self, preprocessor, cell_mock, mess }] assert 'foobar' in preprocessor._display_id_map - @ExecuteTestBase.prepare_cell_mocks({ + @prepare_cell_mocks({ 'msg_type': 'display_data', 'header': {'msg_type': 'display_data'}, 'content': {'metadata': {'metafoo': 'metabar'}, 'data': {'foo': 'bar'}} @@ -776,7 +777,7 @@ def test_display_data_without_id_message(self, preprocessor, cell_mock, message_ # No display id was provided assert not preprocessor._display_id_map - @ExecuteTestBase.prepare_cell_mocks({ + @prepare_cell_mocks({ 'msg_type': 'display_data', 'header': {'msg_type': 'display_data'}, 'content': { @@ -797,7 +798,7 @@ def test_display_data_message(self, preprocessor, cell_mock, message_mock): }] assert 'foobar' in preprocessor._display_id_map - @ExecuteTestBase.prepare_cell_mocks({ + @prepare_cell_mocks({ 'msg_type': 'display_data', 'header': {'msg_type': 'display_data'}, 'content': { @@ -842,7 +843,7 @@ def test_display_data_same_id_message(self, preprocessor, cell_mock, message_moc }] assert 'foobar' in preprocessor._display_id_map - @ExecuteTestBase.prepare_cell_mocks({ + @prepare_cell_mocks({ 'msg_type': 'update_display_data', 'header': {'msg_type': 'update_display_data'}, 'content': {'metadata': {'metafoo': 'metabar'}, 'data': {'foo': 'bar'}} @@ -856,7 +857,7 @@ def test_update_display_data_without_id_message(self, preprocessor, cell_mock, m # No display id was provided assert not preprocessor._display_id_map - @ExecuteTestBase.prepare_cell_mocks({ + @prepare_cell_mocks({ 'msg_type': 'display_data', 'header': {'msg_type': 'display_data'}, 'content': { @@ -885,7 +886,7 @@ def test_update_display_data_mismatch_id_message(self, preprocessor, cell_mock, }] assert 'foobar' in preprocessor._display_id_map - @ExecuteTestBase.prepare_cell_mocks({ + @prepare_cell_mocks({ 'msg_type': 'display_data', 'header': {'msg_type': 'display_data'}, 'content': { @@ -914,7 +915,7 @@ def test_update_display_data_message(self, preprocessor, cell_mock, message_mock }] assert 'foobar' in preprocessor._display_id_map - @ExecuteTestBase.prepare_cell_mocks({ + @prepare_cell_mocks({ 'msg_type': 'error', 'header': {'msg_type': 'error'}, 'content': {'ename': 'foo', 'evalue': 'bar', 'traceback': ['Boom']} From 984d16bfc6d330ff37583d4d0f57b5e28e200137 Mon Sep 17 00:00:00 2001 From: Matthew Seal Date: Mon, 6 May 2019 12:31:23 -0400 Subject: [PATCH 297/671] Fixed rename issues in test_execute --- nbconvert/preprocessors/tests/test_execute.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/nbconvert/preprocessors/tests/test_execute.py b/nbconvert/preprocessors/tests/test_execute.py index 9d6e26b7b..59985e204 100644 --- a/nbconvert/preprocessors/tests/test_execute.py +++ b/nbconvert/preprocessors/tests/test_execute.py @@ -139,7 +139,7 @@ def iopub_messages_mock(): return MagicMock( side_effect=[ # Default the parent_header so mocks don't need to include this - ExecuteTestBase.merge_dicts( + PreprocessorTestsBase.merge_dicts( {'parent_header': {'msg_id': parent_id}}, msg) for msg in messages ] @@ -156,7 +156,7 @@ def test_mock_wrapper(self): processes them. """ cell_mock = NotebookNode(source='"foo" = "bar"', outputs=[]) - preprocessor = self.build_preprocessor({}) + preprocessor = build_preprocessor({}) preprocessor.nb = {'cells': [cell_mock]} # self.kc.iopub_channel.get_msg => message_mock.side_effect[i] @@ -254,10 +254,10 @@ class TestExecute(PreprocessorTestsBase): def test_constructor(self): """Can a ExecutePreprocessor be constructed?""" - self.build_preprocessor({}) + build_preprocessor({}) def test_populate_language_info(self): - preprocessor = self.build_preprocessor(opts=dict(kernel_name="python")) + preprocessor = build_preprocessor(opts=dict(kernel_name="python")) nb = nbformat.v4.new_notebook() # Certainly has no language_info. nb, _ = preprocessor.preprocess(nb, resources={}) assert 'language_info' in nb.metadata @@ -332,7 +332,7 @@ def test_kernel_death(self): res = self.build_resources() res['metadata']['path'] = os.path.dirname(filename) - preprocessor = self.build_preprocessor({"timeout": 5}) + preprocessor = build_preprocessor({"timeout": 5}) try: input_nb, output_nb = preprocessor(input_nb, {}) @@ -386,7 +386,7 @@ def test_custom_kernel_manager(self): with io.open(filename) as f: input_nb = nbformat.read(f, 4) - preprocessor = self.build_preprocessor({ + preprocessor = build_preprocessor({ 'kernel_manager_class': FakeCustomKernelManager }) From 1db7f23ac3c605a4715c1d6131374284b287bb0a Mon Sep 17 00:00:00 2001 From: Matthew Seal Date: Mon, 6 May 2019 14:55:12 -0400 Subject: [PATCH 298/671] Fixed python 2 tests --- .../tests/files/Clear Output.ipynb | 31 ++++++++++++++----- nbconvert/preprocessors/tests/test_execute.py | 4 ++- 2 files changed, 26 insertions(+), 9 deletions(-) diff --git a/nbconvert/preprocessors/tests/files/Clear Output.ipynb b/nbconvert/preprocessors/tests/files/Clear Output.ipynb index 45328e99c..88812a167 100644 --- a/nbconvert/preprocessors/tests/files/Clear Output.ipynb +++ b/nbconvert/preprocessors/tests/files/Clear Output.ipynb @@ -3,20 +3,17 @@ { "cell_type": "code", "execution_count": 1, - "metadata": { - "collapsed": false - }, + "metadata": {}, "outputs": [], "source": [ + "from __future__ import print_function\n", "from IPython.display import clear_output" ] }, { "cell_type": "code", "execution_count": 2, - "metadata": { - "collapsed": false - }, + "metadata": {}, "outputs": [ { "name": "stdout", @@ -185,7 +182,25 @@ ] } ], - "metadata": {}, + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.6.7" + } + }, "nbformat": 4, - "nbformat_minor": 0 + "nbformat_minor": 1 } diff --git a/nbconvert/preprocessors/tests/test_execute.py b/nbconvert/preprocessors/tests/test_execute.py index 59985e204..163bcdea6 100644 --- a/nbconvert/preprocessors/tests/test_execute.py +++ b/nbconvert/preprocessors/tests/test_execute.py @@ -21,6 +21,7 @@ from .base import PreprocessorTestsBase from ..execute import ExecutePreprocessor, CellExecutionError, executenb, DeadKernelError +from ...exporters.exporter import ResourcesDict import IPython from mock import MagicMock @@ -242,7 +243,8 @@ def assert_notebooks_equal(expected, actual): def test_run_all_notebooks(input_name, opts): """Runs a series of test notebooks and compares them to their actual output""" input_file = os.path.join(current_dir, 'files', input_name) - res = PreprocessorTestsBase().build_resources() + res = ResourcesDict() + res['metadata'] = ResourcesDict() res['metadata']['path'] = os.path.join(current_dir, 'files') input_nb, output_nb = run_notebook(input_file, opts, res) assert_notebooks_equal(input_nb, output_nb) From cb2ef94840731f34a748860d984d4dca342d88be Mon Sep 17 00:00:00 2001 From: Alex Rudy Date: Mon, 6 May 2019 13:32:38 -0400 Subject: [PATCH 299/671] Disable IPython history when preprocessing IPython saves history in an SQLite DB. This can break in unfortunate ways when running multiple IPython kernels simultaneously. NBConvert will now default to disabling history saving in IPython kernels. --- nbconvert/preprocessors/execute.py | 16 +++++++ .../tests/files/Check History in Memory.ipynb | 46 +++++++++++++++++++ nbconvert/preprocessors/tests/test_execute.py | 1 + 3 files changed, 63 insertions(+) create mode 100644 nbconvert/preprocessors/tests/files/Check History in Memory.ipynb diff --git a/nbconvert/preprocessors/execute.py b/nbconvert/preprocessors/execute.py index 2e432819b..1df2edc77 100644 --- a/nbconvert/preprocessors/execute.py +++ b/nbconvert/preprocessors/execute.py @@ -220,6 +220,20 @@ class ExecutePreprocessor(Preprocessor): ) ).tag(config=True) + ipython_hist_file = Unicode( + default_value=':memory:', + help="""Path to file to use for SQLite history database for an IPython kernel. + + The specific value `:memory:` (including the colon + at both end but not the back ticks), avoids creating a history file. Otherwise, IPython + will create a history file for each kernel. + + When running kernels simultaneously (e.g. via multiprocessing) saving history a single + SQLite file can result in database errors, so using `:memory:` is recommended in non-interactive + contexts. + + """).tag(config=True) + kernel_manager_class = Type( config=True, help='The kernel manager class to use.' @@ -268,6 +282,8 @@ def start_new_kernel(self, **kwargs): 'kernelspec', {}).get('name', 'python') km = self.kernel_manager_class(kernel_name=self.kernel_name, config=self.config) + if km.ipykernel and self.ipython_hist_file: + self.extra_arguments += ['--HistoryManager.hist_file={}'.format(self.ipython_hist_file)] km.start_kernel(extra_arguments=self.extra_arguments, **kwargs) kc = km.client() diff --git a/nbconvert/preprocessors/tests/files/Check History in Memory.ipynb b/nbconvert/preprocessors/tests/files/Check History in Memory.ipynb new file mode 100644 index 000000000..6b0c58c91 --- /dev/null +++ b/nbconvert/preprocessors/tests/files/Check History in Memory.ipynb @@ -0,0 +1,46 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "from IPython import get_ipython" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": { + "scrolled": true + }, + "outputs": [], + "source": [ + "ip = get_ipython()\n", + "assert ip.history_manager.hist_file == ':memory:'" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.7.0" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/nbconvert/preprocessors/tests/test_execute.py b/nbconvert/preprocessors/tests/test_execute.py index 163bcdea6..a7c711695 100644 --- a/nbconvert/preprocessors/tests/test_execute.py +++ b/nbconvert/preprocessors/tests/test_execute.py @@ -238,6 +238,7 @@ def assert_notebooks_equal(expected, actual): ("Unicode.ipynb", dict(kernel_name="python")), ("UnicodePy3.ipynb", dict(kernel_name="python")), ("update-display-id.ipynb", dict(kernel_name="python")), + ("Check History in Memory.ipynb", dict(kernel_name="python")), ] ) def test_run_all_notebooks(input_name, opts): From e348ee153f82d060bf61b50b5fd0873ca829546f Mon Sep 17 00:00:00 2001 From: Alex Rudy Date: Tue, 7 May 2019 09:43:26 -0400 Subject: [PATCH 300/671] Test for parallel execution of notebooks --- .../tests/files/Parallel Execute.ipynb | 84 +++++++++++++++++++ nbconvert/preprocessors/tests/test_execute.py | 69 +++++++++++++-- 2 files changed, 148 insertions(+), 5 deletions(-) create mode 100644 nbconvert/preprocessors/tests/files/Parallel Execute.ipynb diff --git a/nbconvert/preprocessors/tests/files/Parallel Execute.ipynb b/nbconvert/preprocessors/tests/files/Parallel Execute.ipynb new file mode 100644 index 000000000..1e49a6d4f --- /dev/null +++ b/nbconvert/preprocessors/tests/files/Parallel Execute.ipynb @@ -0,0 +1,84 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "import os.path\n", + "import tempfile\n", + "import time" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "other_notebook = {'A':'B', 'B':'A'}[this_notebook]\n", + "directory = os.environ['NBEXECUTE_TEST_PARALLEL_TMPDIR']\n", + "with open(os.path.join(directory, 'test_file_{}.txt'.format(this_notebook)), 'w') as f:\n", + " f.write('Hello from {}'.format(this_notebook))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "start = time.time()\n", + "timeout = 5\n", + "end = start + timeout\n", + "target_file = os.path.join(directory, 'test_file_{}.txt'.format(other_notebook))\n", + "while time.time() < end:\n", + " time.sleep(0.1)\n", + " if os.path.exists(target_file):\n", + " with open(target_file, 'r') as f:\n", + " text = f.read()\n", + " if text == 'Hello from {}'.format(other_notebook):\n", + " break\n", + "else:\n", + " assert False, \"Timed out – didn't get a message from {}\".format(other_notebook)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.7.0" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/nbconvert/preprocessors/tests/test_execute.py b/nbconvert/preprocessors/tests/test_execute.py index a7c711695..3d3716382 100644 --- a/nbconvert/preprocessors/tests/test_execute.py +++ b/nbconvert/preprocessors/tests/test_execute.py @@ -12,7 +12,10 @@ import glob import io import os +import logging import re +import threading +import multiprocessing as mp import nbformat import sys @@ -69,7 +72,7 @@ def build_preprocessor(opts): return preprocessor -def run_notebook(filename, opts, resources): +def run_notebook(filename, opts, resources, preprocess_notebook=None): """Loads and runs a notebook, returning both the version prior to running it and the version after running it. @@ -77,6 +80,9 @@ def run_notebook(filename, opts, resources): with io.open(filename) as f: input_nb = nbformat.read(f, 4) + if preprocess_notebook: + input_nb = preprocess_notebook(input_nb) + preprocessor = build_preprocessor(opts) cleaned_input_nb = copy.deepcopy(input_nb) for cell in cleaned_input_nb.cells: @@ -218,6 +224,12 @@ def assert_notebooks_equal(expected, actual): actual_execution_count = actual_cell.get('execution_count', None) assert expected_execution_count == actual_execution_count +def notebook_resources(): + res = ResourcesDict() + res['metadata'] = ResourcesDict() + res['metadata']['path'] = os.path.join(current_dir, 'files') + return res + @pytest.mark.parametrize( ["input_name", "opts"], @@ -244,13 +256,60 @@ def assert_notebooks_equal(expected, actual): def test_run_all_notebooks(input_name, opts): """Runs a series of test notebooks and compares them to their actual output""" input_file = os.path.join(current_dir, 'files', input_name) - res = ResourcesDict() - res['metadata'] = ResourcesDict() - res['metadata']['path'] = os.path.join(current_dir, 'files') - input_nb, output_nb = run_notebook(input_file, opts, res) + input_nb, output_nb = run_notebook(input_file, opts, notebook_resources()) assert_notebooks_equal(input_nb, output_nb) +def label_parallel_notebook(nb, label): + """Insert a cell in a notebook which sets the variable `this_notebook` to the string `label`. + + Used for parallel testing to label two notebooks which are run simultaneously. + """ + label_cell = nbformat.NotebookNode( + { + "cell_type": "code", + "execution_count": None, + "metadata": {}, + "outputs": [], + "source": "this_notebook = '{}'".format(label), + } + ) + + nb.cells.insert(1, label_cell) + return nb + + +def test_parallel_notebooks(capfd, tmpdir): + """Two notebooks should be able to be run simultaneously without problems. + + The two notebooks spawned here use the filesystem to check that the other notebook + wrote to the filesystem.""" + + opts = dict(kernel_name="python") + input_name = "Parallel Execute.ipynb" + input_file = os.path.join(current_dir, "files", input_name) + res = notebook_resources() + + with modified_env({"NBEXECUTE_TEST_PARALLEL_TMPDIR": str(tmpdir)}): + threads = [ + threading.Thread( + target=run_notebook, + args=( + input_file, + opts, + res, + functools.partial(label_parallel_notebook, label=label), + ), + ) + for label in ("A", "B") + ] + [t.start() for t in threads] + [t.join(timeout=2) for t in threads] + + captured = capfd.readouterr() + assert captured.err == "" + + class TestExecute(PreprocessorTestsBase): """Contains test functions for execute.py""" maxDiff = None From e296eb5f93cc1ecf0f1df949251aa0cdb140e4be Mon Sep 17 00:00:00 2001 From: Alex Rudy Date: Wed, 8 May 2019 10:12:14 -0400 Subject: [PATCH 301/671] Fixup comments on parallel notebook test --- .../tests/files/Parallel Execute.ipynb | 27 +++++++++---------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/nbconvert/preprocessors/tests/files/Parallel Execute.ipynb b/nbconvert/preprocessors/tests/files/Parallel Execute.ipynb index 1e49a6d4f..1877db6fd 100644 --- a/nbconvert/preprocessors/tests/files/Parallel Execute.ipynb +++ b/nbconvert/preprocessors/tests/files/Parallel Execute.ipynb @@ -1,5 +1,17 @@ { "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Ensure notebooks can execute in parallel\n", + "\n", + "This notebook uses a file system based \"lock\" to assert that two instances of the notebook kernel will run in parallel. Each instance writes to a file in a temporary directory, and then tries to read the other file from\n", + "the temporary directory, so that running them in sequence will fail, but running them in parallel will succed.\n", + "\n", + "Two notebooks are launched, each with an injected cell which sets the `this_notebook` variable. One notebook is set to `this_notebook = 'A'` and the other `this_notebook = 'B'`." + ] + }, { "cell_type": "code", "execution_count": null, @@ -18,6 +30,7 @@ "metadata": {}, "outputs": [], "source": [ + "# the variable this_notebook is injectected in a cell above by the test framework.\n", "other_notebook = {'A':'B', 'B':'A'}[this_notebook]\n", "directory = os.environ['NBEXECUTE_TEST_PARALLEL_TMPDIR']\n", "with open(os.path.join(directory, 'test_file_{}.txt'.format(this_notebook)), 'w') as f:\n", @@ -44,20 +57,6 @@ "else:\n", " assert False, \"Timed out – didn't get a message from {}\".format(other_notebook)" ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] } ], "metadata": { From 53653cb438a9979e9ece32f70c8befc3de26dfd3 Mon Sep 17 00:00:00 2001 From: Alex Rudy Date: Wed, 8 May 2019 17:56:57 -0700 Subject: [PATCH 302/671] Remove kernel specification --- .../tests/files/Parallel Execute.ipynb | 20 +------------------ 1 file changed, 1 insertion(+), 19 deletions(-) diff --git a/nbconvert/preprocessors/tests/files/Parallel Execute.ipynb b/nbconvert/preprocessors/tests/files/Parallel Execute.ipynb index 1877db6fd..d40545c30 100644 --- a/nbconvert/preprocessors/tests/files/Parallel Execute.ipynb +++ b/nbconvert/preprocessors/tests/files/Parallel Execute.ipynb @@ -59,25 +59,7 @@ ] } ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.7.0" - } - }, + "metadata": {}, "nbformat": 4, "nbformat_minor": 2 } From fb3780bbf96229e3ed7cc7f1d0771ad7b85fbe17 Mon Sep 17 00:00:00 2001 From: Alex Rudy Date: Wed, 8 May 2019 17:59:16 -0700 Subject: [PATCH 303/671] Remove kernel spec from additional notebook --- .../tests/files/Check History in Memory.ipynb | 20 +------------------ 1 file changed, 1 insertion(+), 19 deletions(-) diff --git a/nbconvert/preprocessors/tests/files/Check History in Memory.ipynb b/nbconvert/preprocessors/tests/files/Check History in Memory.ipynb index 6b0c58c91..7dc65a1a3 100644 --- a/nbconvert/preprocessors/tests/files/Check History in Memory.ipynb +++ b/nbconvert/preprocessors/tests/files/Check History in Memory.ipynb @@ -22,25 +22,7 @@ ] } ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.7.0" - } - }, + "metadata": {}, "nbformat": 4, "nbformat_minor": 2 } From 282945d6602683732166201eb99b0c994b7bfa07 Mon Sep 17 00:00:00 2001 From: Carol Willing Date: Fri, 10 May 2019 15:50:55 -0700 Subject: [PATCH 304/671] bump python version for docs build --- docs/environment.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/environment.yml b/docs/environment.yml index d70cb770b..a418f89b8 100644 --- a/docs/environment.yml +++ b/docs/environment.yml @@ -2,7 +2,7 @@ name: nbconvert_docs channels: - conda-forge dependencies: -- python==3.5 +- python==3.7 - pandoc - nbformat - jupyter_client From 9a0080bc1348735cc461eeb8b67a2cdffb1f870f Mon Sep 17 00:00:00 2001 From: Carol Willing Date: Fri, 10 May 2019 15:58:09 -0700 Subject: [PATCH 305/671] migrate rtd yaml to version 2 --- .readthedocs.yml | 20 ++++++++++++++++++++ readthedocs.yml | 5 ----- 2 files changed, 20 insertions(+), 5 deletions(-) create mode 100644 .readthedocs.yml delete mode 100644 readthedocs.yml diff --git a/.readthedocs.yml b/.readthedocs.yml new file mode 100644 index 000000000..cdd350487 --- /dev/null +++ b/.readthedocs.yml @@ -0,0 +1,20 @@ +# .readthedocs.yml +# Read the Docs configuration file +# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details + +# Required +version: 2 + +sphinx: + configuration: docs/source/conf.py + +formats: all + +conda: + environment: docs/environment.yml + +build: + image: latest + +python: + version: 3.7 diff --git a/readthedocs.yml b/readthedocs.yml deleted file mode 100644 index 99b712dcb..000000000 --- a/readthedocs.yml +++ /dev/null @@ -1,5 +0,0 @@ -conda: - file: docs/environment.yml -python: - version: 3.5 - pip_install: true From 50497f392563d183a2e679e561a5beaf848ee657 Mon Sep 17 00:00:00 2001 From: Matthew Seal Date: Fri, 10 May 2019 16:31:56 -0700 Subject: [PATCH 306/671] Fixed manifest issue --- MANIFEST.in | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MANIFEST.in b/MANIFEST.in index 0bec47736..9de7721a0 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -9,7 +9,7 @@ include nbconvert/tests/README.md # Documentation graft docs exclude docs/\#* -exclude readthedocs.yml +exclude .readthedocs.yml exclude codecov.yml # Examples From 4140b66a2589e51836c9d0954129c901ed70e049 Mon Sep 17 00:00:00 2001 From: 00Kai0 Date: Sat, 11 May 2019 15:13:40 +0800 Subject: [PATCH 307/671] kwargs is used when km is exist, but not defind in func. --- nbconvert/preprocessors/execute.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/nbconvert/preprocessors/execute.py b/nbconvert/preprocessors/execute.py index 2e432819b..8b7f861a4 100644 --- a/nbconvert/preprocessors/execute.py +++ b/nbconvert/preprocessors/execute.py @@ -282,7 +282,7 @@ def start_new_kernel(self, **kwargs): return km, kc @contextmanager - def setup_preprocessor(self, nb, resources, km=None): + def setup_preprocessor(self, nb, resources, km=None, **kwargs): """ Context manager for setting up the class to execute a notebook. @@ -321,7 +321,8 @@ def setup_preprocessor(self, nb, resources, km=None): self.widget_buffers = {} if km is None: - self.km, self.kc = self.start_new_kernel(cwd=path) + kwargs["cwd"] = path + self.km, self.kc = self.start_new_kernel(**kwargs) try: # Yielding unbound args for more easier understanding and downstream consumption yield nb, self.km, self.kc From ed10180bc4907540fa42649008b664a80815fe62 Mon Sep 17 00:00:00 2001 From: "Jessica B. Hamrick" Date: Sun, 12 May 2019 21:54:08 +0100 Subject: [PATCH 308/671] Use default argument of empty dictionary for cell context --- nbconvert/exporters/html.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nbconvert/exporters/html.py b/nbconvert/exporters/html.py index 53f627cbe..a950b663e 100644 --- a/nbconvert/exporters/html.py +++ b/nbconvert/exporters/html.py @@ -71,7 +71,7 @@ def default_config(self): @contextfilter def markdown2html(self, context, source): """Markdown to HTML filter respecting the anchor_link_text setting""" - cell = context['cell'] + cell = context.get('cell', {}) attachments = cell.get('attachments', {}) renderer = IPythonRenderer(escape=False, attachments=attachments, anchor_link_text=self.anchor_link_text) From 036b9cd0c06c7118fa39a8ed92bbbd09353a0559 Mon Sep 17 00:00:00 2001 From: amniskin Date: Sun, 12 May 2019 17:44:42 -0700 Subject: [PATCH 309/671] dedenting html in ExtractOutputPreprocessor Due to HTML being white-space invariant, in order for this to play well with other tools we dedent the html cell data. --- nbconvert/preprocessors/extractoutput.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/nbconvert/preprocessors/extractoutput.py b/nbconvert/preprocessors/extractoutput.py index 63280b7ee..eb8c8594e 100755 --- a/nbconvert/preprocessors/extractoutput.py +++ b/nbconvert/preprocessors/extractoutput.py @@ -5,6 +5,7 @@ # Copyright (c) IPython Development Team. # Distributed under the terms of the Modified BSD License. +from textwrap import dedent from binascii import a2b_base64 import sys import os @@ -82,6 +83,8 @@ def preprocess_cell(self, cell, resources, cell_index): for index, out in enumerate(cell.get('outputs', [])): if out.output_type not in {'display_data', 'execute_result'}: continue + if 'text/html' in out.data: + out['data']['text/html'] = dedent(out['data']['text/html']) #Get the output in data formats that the template needs extracted for mime_type in self.extract_output_types: if mime_type in out.data: From 7b4a67711fc1df56d6aeb31a3e75cd63a466968a Mon Sep 17 00:00:00 2001 From: Alex Rudy Date: Mon, 13 May 2019 10:53:22 -0700 Subject: [PATCH 310/671] Review fixes --- nbconvert/preprocessors/tests/test_execute.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nbconvert/preprocessors/tests/test_execute.py b/nbconvert/preprocessors/tests/test_execute.py index 3d3716382..c9aa20b4f 100644 --- a/nbconvert/preprocessors/tests/test_execute.py +++ b/nbconvert/preprocessors/tests/test_execute.py @@ -12,7 +12,6 @@ import glob import io import os -import logging import re import threading import multiprocessing as mp @@ -225,6 +224,7 @@ def assert_notebooks_equal(expected, actual): assert expected_execution_count == actual_execution_count def notebook_resources(): + """Prepare a notebook resources dictionary for executing test notebooks in the `files` folder.""" res = ResourcesDict() res['metadata'] = ResourcesDict() res['metadata']['path'] = os.path.join(current_dir, 'files') From 7b115b649806b46a9f1c75dec0c4f12aee42e72a Mon Sep 17 00:00:00 2001 From: Dustin H Date: Mon, 13 May 2019 16:10:05 -0400 Subject: [PATCH 311/671] Remove ExecuteTestBase in the new tests --- nbconvert/preprocessors/tests/test_execute.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/nbconvert/preprocessors/tests/test_execute.py b/nbconvert/preprocessors/tests/test_execute.py index 12cd9b4c3..0ba634a7f 100644 --- a/nbconvert/preprocessors/tests/test_execute.py +++ b/nbconvert/preprocessors/tests/test_execute.py @@ -515,7 +515,7 @@ def test_busy_message(self, preprocessor, cell_mock, message_mock): # Ensure no outputs were generated assert cell_mock.outputs == [] - @ExecuteTestBase.prepare_cell_mocks({ + @prepare_cell_mocks({ 'msg_type': 'stream', 'header': {'msg_type': 'stream'}, 'content': {'name': 'stdout', 'text': 'foo'}, @@ -539,7 +539,7 @@ def test_deadline_exec_reply(self, preprocessor, cell_mock, message_mock): {'output_type': 'stream', 'name': 'stderr', 'text': 'bar'} ]) - @ExecuteTestBase.prepare_cell_mocks() + @prepare_cell_mocks() def test_deadline_iopub(self, preprocessor, cell_mock, message_mock): # The shell_channel will complete, so we expect only to hit the iopub timeout. message_mock.side_effect = Empty() @@ -548,7 +548,7 @@ def test_deadline_iopub(self, preprocessor, cell_mock, message_mock): with pytest.raises(TimeoutError): preprocessor.run_cell(cell_mock) - @ExecuteTestBase.prepare_cell_mocks({ + @prepare_cell_mocks({ 'msg_type': 'stream', 'header': {'msg_type': 'stream'}, 'content': {'name': 'stdout', 'text': 'foo'}, @@ -574,7 +574,7 @@ def test_eventual_deadline_iopub(self, preprocessor, cell_mock, message_mock): {'output_type': 'stream', 'name': 'stderr', 'text': 'bar'} ]) - @ExecuteTestBase.prepare_cell_mocks({ + @prepare_cell_mocks({ 'msg_type': 'execute_input', 'header': {'msg_type': 'execute_input'}, 'content': {} From 0fb933d2e60116eadef2c8cb5a0f6d3c76da22ab Mon Sep 17 00:00:00 2001 From: Dustin H Date: Mon, 13 May 2019 16:19:59 -0400 Subject: [PATCH 312/671] fix indentation level --- nbconvert/preprocessors/tests/test_execute.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/nbconvert/preprocessors/tests/test_execute.py b/nbconvert/preprocessors/tests/test_execute.py index 0ba634a7f..fcf9bfc98 100644 --- a/nbconvert/preprocessors/tests/test_execute.py +++ b/nbconvert/preprocessors/tests/test_execute.py @@ -515,7 +515,7 @@ def test_busy_message(self, preprocessor, cell_mock, message_mock): # Ensure no outputs were generated assert cell_mock.outputs == [] - @prepare_cell_mocks({ + @prepare_cell_mocks({ 'msg_type': 'stream', 'header': {'msg_type': 'stream'}, 'content': {'name': 'stdout', 'text': 'foo'}, @@ -539,7 +539,7 @@ def test_deadline_exec_reply(self, preprocessor, cell_mock, message_mock): {'output_type': 'stream', 'name': 'stderr', 'text': 'bar'} ]) - @prepare_cell_mocks() + @prepare_cell_mocks() def test_deadline_iopub(self, preprocessor, cell_mock, message_mock): # The shell_channel will complete, so we expect only to hit the iopub timeout. message_mock.side_effect = Empty() @@ -548,7 +548,7 @@ def test_deadline_iopub(self, preprocessor, cell_mock, message_mock): with pytest.raises(TimeoutError): preprocessor.run_cell(cell_mock) - @prepare_cell_mocks({ + @prepare_cell_mocks({ 'msg_type': 'stream', 'header': {'msg_type': 'stream'}, 'content': {'name': 'stdout', 'text': 'foo'}, @@ -574,7 +574,7 @@ def test_eventual_deadline_iopub(self, preprocessor, cell_mock, message_mock): {'output_type': 'stream', 'name': 'stderr', 'text': 'bar'} ]) - @prepare_cell_mocks({ + @prepare_cell_mocks({ 'msg_type': 'execute_input', 'header': {'msg_type': 'execute_input'}, 'content': {} From 84e670a6a3326016ad1594d84cb131c2d72cfaa2 Mon Sep 17 00:00:00 2001 From: Tyler Makaro Date: Mon, 13 May 2019 14:10:28 -0700 Subject: [PATCH 313/671] improve prompt alignment and font --- nbconvert/templates/latex/style_jupyter.tplx | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/nbconvert/templates/latex/style_jupyter.tplx b/nbconvert/templates/latex/style_jupyter.tplx index fcdfeb957..3114cb930 100644 --- a/nbconvert/templates/latex/style_jupyter.tplx +++ b/nbconvert/templates/latex/style_jupyter.tplx @@ -100,9 +100,12 @@ ((*- endblock style_colors *)) % prompt + \makeatletter + \newcommand{\boxspacing}{\kern\kvtcb@left@rule\kern\kvtcb@boxsep} + \makeatother ((*- block style_prompt *)) \newcommand{\prompt}[4]{ - \llap{{\color{#2}[#3]: #4}}\vspace{-1.25em} + \ttfamily\llap{{\color{#2}[#3]:\hspace{3pt}#4}}\vspace{-\baselineskip} } ((* endblock style_prompt *)) @@ -113,7 +116,7 @@ %=============================================================================== ((* block input scoped *)) - ((( draw_cell(cell.source | highlight_code(strip_verbatim=True), cell, 'In', 'incolor', '\\hspace{4pt}') ))) + ((( draw_cell(cell.source | highlight_code(strip_verbatim=True), cell, 'In', 'incolor', '\\boxspacing') ))) ((* endblock input *)) @@ -128,7 +131,7 @@ ((* block execute_result scoped *)) ((*- for type in output.data | filter_data_type -*)) ((*- if type in ['text/plain']*)) - ((( draw_cell(output.data['text/plain'] | wrap_text(charlim) | escape_latex, cell, 'Out', 'outcolor', '\\hspace{3.5pt}') ))) + ((( draw_cell(output.data['text/plain'] | wrap_text(charlim) | escape_latex, cell, 'Out', 'outcolor', '\\boxspacing') ))) ((* else -*)) ((( " " ))) ((( draw_prompt(cell, 'Out', 'outcolor','') )))((( super() ))) @@ -152,7 +155,7 @@ ((* macro draw_cell(text, cell, prompt, prompt_color, extra_space) -*)) ((*- if prompt == 'In' -*)) ((*- set style = "breakable, size=fbox, boxrule=1pt, pad at break*=1mm,colback=cellbackground, colframe=cellborder"-*)) -((*- else -*))((*- set style = "breakable, boxrule=.5pt, size=fbox, pad at break*=1mm, opacityfill=0"-*))((*- endif -*)) +((*- else -*))((*- set style = "breakable, size=fbox, boxrule=.5pt, pad at break*=1mm, opacityfill=0"-*))((*- endif -*)) \begin{tcolorbox}[((( style )))] (((- draw_prompt(cell, prompt, prompt_color, extra_space) ))) From be675f2a352fddf702368b8fc6ef433ae58fbf8f Mon Sep 17 00:00:00 2001 From: Tyler Makaro Date: Mon, 13 May 2019 14:21:05 -0700 Subject: [PATCH 314/671] allow boxes to have spacing before/after --- nbconvert/templates/latex/style_jupyter.tplx | 1 - 1 file changed, 1 deletion(-) diff --git a/nbconvert/templates/latex/style_jupyter.tplx b/nbconvert/templates/latex/style_jupyter.tplx index 3114cb930..76173d168 100644 --- a/nbconvert/templates/latex/style_jupyter.tplx +++ b/nbconvert/templates/latex/style_jupyter.tplx @@ -3,7 +3,6 @@ ((*- block packages -*)) \usepackage[breakable]{tcolorbox} - \tcbset{nobeforeafter} % prevents tcolorboxes being placing in paragraphs \usepackage{float} \floatplacement{figure}{H} % forces figures to be placed at the correct location ((( super() ))) From b03a44e629558d11ddd9e38ea934402880f01b03 Mon Sep 17 00:00:00 2001 From: Tyler Makaro Date: Mon, 13 May 2019 14:56:40 -0700 Subject: [PATCH 315/671] supress auto-indenting --- nbconvert/templates/latex/style_jupyter.tplx | 1 + 1 file changed, 1 insertion(+) diff --git a/nbconvert/templates/latex/style_jupyter.tplx b/nbconvert/templates/latex/style_jupyter.tplx index 76173d168..dc4bfad7c 100644 --- a/nbconvert/templates/latex/style_jupyter.tplx +++ b/nbconvert/templates/latex/style_jupyter.tplx @@ -3,6 +3,7 @@ ((*- block packages -*)) \usepackage[breakable]{tcolorbox} + \usepackage{parskip} % Stop auto-indenting (to mimic markdown behaviour) \usepackage{float} \floatplacement{figure}{H} % forces figures to be placed at the correct location ((( super() ))) From 20b9e06727028883cac5bfc2f25ed06e55934dd2 Mon Sep 17 00:00:00 2001 From: Tyler Makaro Date: Mon, 13 May 2019 15:20:54 -0700 Subject: [PATCH 316/671] improve title --- nbconvert/templates/latex/base.tplx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/nbconvert/templates/latex/base.tplx b/nbconvert/templates/latex/base.tplx index 1fb5d8889..9fa58ff9b 100644 --- a/nbconvert/templates/latex/base.tplx +++ b/nbconvert/templates/latex/base.tplx @@ -59,6 +59,9 @@ This template does not define a docclass, the inheriting class must define this. % internal navigation ('pdf bookmarks' for the table of contents, % internal cross-reference links, web links for URLs, etc.) \usepackage{hyperref} + % The default LaTeX title has an obnoxious amount of whitespace. By default, + % titling removes some of it. It also provides customization options. + \usepackage{titling} \usepackage{longtable} % longtable support required by pandoc >1.10 \usepackage{booktabs} % table support for pandoc > 1.12.2 \usepackage[inline]{enumitem} % IRkernel/repr support (it uses the enumerate* environment) From 0c7d7ba3b6b05d14cade2b76525a525e559f7fcc Mon Sep 17 00:00:00 2001 From: Alex Rudy Date: Mon, 13 May 2019 16:16:13 -0700 Subject: [PATCH 317/671] Use nbformat to set up notebook cells --- nbconvert/preprocessors/tests/test_execute.py | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/nbconvert/preprocessors/tests/test_execute.py b/nbconvert/preprocessors/tests/test_execute.py index c9aa20b4f..0229aa52e 100644 --- a/nbconvert/preprocessors/tests/test_execute.py +++ b/nbconvert/preprocessors/tests/test_execute.py @@ -265,16 +265,7 @@ def label_parallel_notebook(nb, label): Used for parallel testing to label two notebooks which are run simultaneously. """ - label_cell = nbformat.NotebookNode( - { - "cell_type": "code", - "execution_count": None, - "metadata": {}, - "outputs": [], - "source": "this_notebook = '{}'".format(label), - } - ) - + label_cell = nbformat.v4.new_code_cell(source="this_notebook = '{}'".format(label)) nb.cells.insert(1, label_cell) return nb From a69287fbdcdd98cb1cdfd8ece179c927a500ef62 Mon Sep 17 00:00:00 2001 From: Tyler Makaro Date: Mon, 13 May 2019 16:39:32 -0700 Subject: [PATCH 318/671] don't hide includegraphics --- nbconvert/templates/latex/base.tplx | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/nbconvert/templates/latex/base.tplx b/nbconvert/templates/latex/base.tplx index 9fa58ff9b..652c753de 100644 --- a/nbconvert/templates/latex/base.tplx +++ b/nbconvert/templates/latex/base.tplx @@ -20,16 +20,6 @@ This template does not define a docclass, the inheriting class must define this. % Basic figure setup, for now with no caption control since it's done % automatically by Pandoc (which extracts ![](path) syntax from Markdown). \usepackage{graphicx} - % We will generate all images so they have a width \maxwidth. This means - % that they will get their normal width if they fit onto the page, but - % are scaled down if they would overflow the margins. - \makeatletter - \def\maxwidth{\ifdim\Gin@nat@width>\linewidth\linewidth - \else\Gin@nat@width\fi} - \makeatother - \let\Oldincludegraphics\includegraphics - % Set max figure width to be 80% of text width, for now hardcoded. - \renewcommand{\includegraphics}[1]{\Oldincludegraphics[width=.8\maxwidth]{#1}} % Ensure that by default, figures have no caption (until we provide a % proper Figure object with a Caption API and a way to capture that % in the conversion process - todo). @@ -37,7 +27,8 @@ This template does not define a docclass, the inheriting class must define this. \DeclareCaptionLabelFormat{nolabel}{} \captionsetup{labelformat=nolabel} - \usepackage{adjustbox} % Used to constrain images to a maximum size + \usepackage[Export]{adjustbox} % Used to constrain images to a maximum size + \adjustboxset{max size={0.9\linewidth}{0.9\paperheight}} \usepackage{xcolor} % Allow colors to be defined \usepackage{enumerate} % Needed for markdown enumerations to work \usepackage{geometry} % Used to adjust the document margins From 739227e4659da404935f3ea9f966fd003a138ab1 Mon Sep 17 00:00:00 2001 From: Tyler Makaro Date: Mon, 13 May 2019 16:54:18 -0700 Subject: [PATCH 319/671] fix whitespace --- nbconvert/templates/latex/style_jupyter.tplx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nbconvert/templates/latex/style_jupyter.tplx b/nbconvert/templates/latex/style_jupyter.tplx index dc4bfad7c..0b1418551 100644 --- a/nbconvert/templates/latex/style_jupyter.tplx +++ b/nbconvert/templates/latex/style_jupyter.tplx @@ -102,7 +102,7 @@ % prompt \makeatletter \newcommand{\boxspacing}{\kern\kvtcb@left@rule\kern\kvtcb@boxsep} - \makeatother + \makeatother ((*- block style_prompt *)) \newcommand{\prompt}[4]{ \ttfamily\llap{{\color{#2}[#3]:\hspace{3pt}#4}}\vspace{-\baselineskip} From 16c28cb7b2bdd24ac460ade26f807e586e57a7c0 Mon Sep 17 00:00:00 2001 From: Tyler Makaro Date: Wed, 15 May 2019 14:25:13 -0700 Subject: [PATCH 320/671] re-add oldincludegraphics --- nbconvert/templates/latex/base.tplx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/nbconvert/templates/latex/base.tplx b/nbconvert/templates/latex/base.tplx index 652c753de..2bad27d45 100644 --- a/nbconvert/templates/latex/base.tplx +++ b/nbconvert/templates/latex/base.tplx @@ -20,6 +20,8 @@ This template does not define a docclass, the inheriting class must define this. % Basic figure setup, for now with no caption control since it's done % automatically by Pandoc (which extracts ![](path) syntax from Markdown). \usepackage{graphicx} + % Maintain compatibility with old templates. Remove in nbconvert 6.0 + \let\Oldincludegraphics\includegraphics % Ensure that by default, figures have no caption (until we provide a % proper Figure object with a Caption API and a way to capture that % in the conversion process - todo). From c57db416e007c841e93c899926f741123f50b3f6 Mon Sep 17 00:00:00 2001 From: Tyler Makaro Date: Wed, 15 May 2019 15:42:43 -0700 Subject: [PATCH 321/671] remove figure captions --- nbconvert/templates/latex/base.tplx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nbconvert/templates/latex/base.tplx b/nbconvert/templates/latex/base.tplx index 2bad27d45..34a1f9563 100644 --- a/nbconvert/templates/latex/base.tplx +++ b/nbconvert/templates/latex/base.tplx @@ -27,7 +27,7 @@ This template does not define a docclass, the inheriting class must define this. % in the conversion process - todo). \usepackage{caption} \DeclareCaptionLabelFormat{nolabel}{} - \captionsetup{labelformat=nolabel} + \captionsetup{format=nolabel} \usepackage[Export]{adjustbox} % Used to constrain images to a maximum size \adjustboxset{max size={0.9\linewidth}{0.9\paperheight}} From 4f4093f425e479ae32a37864de92b355911eb538 Mon Sep 17 00:00:00 2001 From: Tyler Makaro Date: Wed, 15 May 2019 15:48:01 -0700 Subject: [PATCH 322/671] move float placement specifier to be used for all cell styles --- nbconvert/templates/latex/base.tplx | 2 ++ nbconvert/templates/latex/style_jupyter.tplx | 2 -- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/nbconvert/templates/latex/base.tplx b/nbconvert/templates/latex/base.tplx index 34a1f9563..3e4347c0d 100644 --- a/nbconvert/templates/latex/base.tplx +++ b/nbconvert/templates/latex/base.tplx @@ -31,6 +31,8 @@ This template does not define a docclass, the inheriting class must define this. \usepackage[Export]{adjustbox} % Used to constrain images to a maximum size \adjustboxset{max size={0.9\linewidth}{0.9\paperheight}} + \usepackage{float} + \floatplacement{figure}{H} % forces figures to be placed at the correct location \usepackage{xcolor} % Allow colors to be defined \usepackage{enumerate} % Needed for markdown enumerations to work \usepackage{geometry} % Used to adjust the document margins diff --git a/nbconvert/templates/latex/style_jupyter.tplx b/nbconvert/templates/latex/style_jupyter.tplx index 0b1418551..dc3843e67 100644 --- a/nbconvert/templates/latex/style_jupyter.tplx +++ b/nbconvert/templates/latex/style_jupyter.tplx @@ -4,8 +4,6 @@ ((*- block packages -*)) \usepackage[breakable]{tcolorbox} \usepackage{parskip} % Stop auto-indenting (to mimic markdown behaviour) - \usepackage{float} - \floatplacement{figure}{H} % forces figures to be placed at the correct location ((( super() ))) ((*- endblock packages -*)) From 45fc47eaf72738c1016ea841fabbf7c9f1f16145 Mon Sep 17 00:00:00 2001 From: Tyler Makaro Date: Wed, 15 May 2019 15:48:57 -0700 Subject: [PATCH 323/671] fix error --- nbconvert/templates/latex/base.tplx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nbconvert/templates/latex/base.tplx b/nbconvert/templates/latex/base.tplx index 3e4347c0d..5f16242cb 100644 --- a/nbconvert/templates/latex/base.tplx +++ b/nbconvert/templates/latex/base.tplx @@ -26,7 +26,7 @@ This template does not define a docclass, the inheriting class must define this. % proper Figure object with a Caption API and a way to capture that % in the conversion process - todo). \usepackage{caption} - \DeclareCaptionLabelFormat{nolabel}{} + \DeclareCaptionFormat{nolabel}{} \captionsetup{format=nolabel} \usepackage[Export]{adjustbox} % Used to constrain images to a maximum size From 7b5e49b4f54ff045d2557a11f294c550e7ba8df0 Mon Sep 17 00:00:00 2001 From: Tyler Makaro Date: Wed, 15 May 2019 16:01:57 -0700 Subject: [PATCH 324/671] remove caption spacing --- nbconvert/templates/latex/base.tplx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nbconvert/templates/latex/base.tplx b/nbconvert/templates/latex/base.tplx index 5f16242cb..dc512bb6c 100644 --- a/nbconvert/templates/latex/base.tplx +++ b/nbconvert/templates/latex/base.tplx @@ -26,8 +26,8 @@ This template does not define a docclass, the inheriting class must define this. % proper Figure object with a Caption API and a way to capture that % in the conversion process - todo). \usepackage{caption} - \DeclareCaptionFormat{nolabel}{} - \captionsetup{format=nolabel} + \DeclareCaptionFormat{nocaption}{} + \captionsetup{format=nocaption,aboveskip=0pt,belowskip=0pt} \usepackage[Export]{adjustbox} % Used to constrain images to a maximum size \adjustboxset{max size={0.9\linewidth}{0.9\paperheight}} From 3f17dce7a8114a0cb90f5cf7375991bf8d1c4d44 Mon Sep 17 00:00:00 2001 From: Dustin H Date: Fri, 17 May 2019 09:53:55 -0400 Subject: [PATCH 325/671] adding comments to explain why we're adding more_output and polling_exec_reply --- nbconvert/preprocessors/execute.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/nbconvert/preprocessors/execute.py b/nbconvert/preprocessors/execute.py index 45951a85b..b6215f414 100644 --- a/nbconvert/preprocessors/execute.py +++ b/nbconvert/preprocessors/execute.py @@ -534,7 +534,16 @@ def run_cell(self, cell, cell_index=0): cell.outputs = [] self.clear_before_next_output = False + # This loop resolves #659. By polling iopub_channel's and shell_channel's + # output we avoid dropping output and important signals (like idle) from + # iopub_channel. Prior to this change, iopub_channel wasn't polled until + # after exec_reply was obtained from shell_channel, leading to the + # aforementioned dropped data. + + # These two variables are used to track what still needs polling: + # more_output=true => continue to poll the iopub_channel more_output = True + # polling_exec_reply=true => continue to poll the shell_channel polling_exec_reply = True while more_output or polling_exec_reply: @@ -543,6 +552,8 @@ def run_cell(self, cell, cell_index=0): polling_exec_reply = False continue + # Avoid exceeding the execution timeout (deadline), but stop + # after at most 1s so we can poll output from iopub_channel. timeout = self._timeout_with_deadline(1, deadline) exec_reply = self._poll_for_reply(parent_msg_id, cell, timeout) if exec_reply is not None: @@ -552,10 +563,14 @@ def run_cell(self, cell, cell_index=0): try: timeout = self.iopub_timeout if polling_exec_reply: + # Avoid exceeding the execution timeout (deadline) while + # polling for output. timeout = self._timeout_with_deadline(timeout, deadline) msg = self.kc.iopub_channel.get_msg(timeout=timeout) except Empty: if polling_exec_reply: + # Still waiting for execution to finish so we expect that + # output may not always be produced yet. continue if self.raise_on_iopub_timeout: From c695364927774b667bc363bf397465fa489710a1 Mon Sep 17 00:00:00 2001 From: Tyler Makaro Date: Fri, 17 May 2019 11:11:49 -0700 Subject: [PATCH 326/671] update export_from_notebook names --- nbconvert/exporters/asciidoc.py | 2 +- nbconvert/exporters/html.py | 2 +- nbconvert/exporters/latex.py | 2 +- nbconvert/exporters/markdown.py | 2 +- nbconvert/exporters/notebook.py | 2 +- nbconvert/exporters/pdf.py | 2 +- nbconvert/exporters/python.py | 1 - nbconvert/exporters/rst.py | 2 +- nbconvert/exporters/script.py | 2 +- nbconvert/exporters/slides.py | 2 +- 10 files changed, 9 insertions(+), 10 deletions(-) diff --git a/nbconvert/exporters/asciidoc.py b/nbconvert/exporters/asciidoc.py index ebc19f4e6..84320d9ef 100644 --- a/nbconvert/exporters/asciidoc.py +++ b/nbconvert/exporters/asciidoc.py @@ -23,7 +23,7 @@ def _template_file_default(self): return 'asciidoc' output_mimetype = 'text/asciidoc' - export_from_notebook = "asciidoc" + export_from_notebook = "AsciiDoc" @default('raw_mimetypes') def _raw_mimetypes_default(self): diff --git a/nbconvert/exporters/html.py b/nbconvert/exporters/html.py index a950b663e..cf6d0871b 100644 --- a/nbconvert/exporters/html.py +++ b/nbconvert/exporters/html.py @@ -23,7 +23,7 @@ class HTMLExporter(TemplateExporter): custom preprocessors/filters. If you don't need custom preprocessors/ filters, just change the 'template_file' config option. """ - export_from_notebook = "html" + export_from_notebook = "HTML" anchor_link_text = Unicode(u'¶', help="The text used as the text for anchor links.").tag(config=True) diff --git a/nbconvert/exporters/latex.py b/nbconvert/exporters/latex.py index 46a6188ae..0c0ee179e 100644 --- a/nbconvert/exporters/latex.py +++ b/nbconvert/exporters/latex.py @@ -21,7 +21,7 @@ class LatexExporter(TemplateExporter): 'template_file' config option. Place your template in the special "/latex" subfolder of the "../templates" folder. """ - export_from_notebook = "latex" + export_from_notebook = "LaTeX" @default('file_extension') def _file_extension_default(self): diff --git a/nbconvert/exporters/markdown.py b/nbconvert/exporters/markdown.py index c465bcf4c..b810d3181 100644 --- a/nbconvert/exporters/markdown.py +++ b/nbconvert/exporters/markdown.py @@ -13,7 +13,7 @@ class MarkdownExporter(TemplateExporter): """ Exports to a markdown document (.md) """ - export_from_notebook = "markdown" + export_from_notebook = "Markdown" @default('file_extension') def _file_extension_default(self): diff --git a/nbconvert/exporters/notebook.py b/nbconvert/exporters/notebook.py index 4462639b2..c39eef536 100644 --- a/nbconvert/exporters/notebook.py +++ b/nbconvert/exporters/notebook.py @@ -26,7 +26,7 @@ def _file_extension_default(self): return '.ipynb' output_mimetype = 'application/json' - export_from_notebook = "notebook" + export_from_notebook = "Notebook" def from_notebook_node(self, nb, resources=None, **kw): nb_copy, resources = super(NotebookExporter, self).from_notebook_node(nb, resources, **kw) diff --git a/nbconvert/exporters/pdf.py b/nbconvert/exporters/pdf.py index 08fc139c7..9f62d8b26 100644 --- a/nbconvert/exporters/pdf.py +++ b/nbconvert/exporters/pdf.py @@ -44,7 +44,7 @@ class PDFExporter(LatexExporter): a temporary directory using the template machinery, and then runs LaTeX to create a pdf. """ - export_from_notebook="pdf" + export_from_notebook="PDF via LaTeX" latex_count = Integer(3, help="How many times latex will be called." diff --git a/nbconvert/exporters/python.py b/nbconvert/exporters/python.py index adbd085c4..642003632 100644 --- a/nbconvert/exporters/python.py +++ b/nbconvert/exporters/python.py @@ -21,4 +21,3 @@ def _template_file_default(self): return 'python.tpl' output_mimetype = 'text/x-python' - export_from_notebook = "python" diff --git a/nbconvert/exporters/rst.py b/nbconvert/exporters/rst.py index f9023b8a4..568144ba9 100644 --- a/nbconvert/exporters/rst.py +++ b/nbconvert/exporters/rst.py @@ -23,7 +23,7 @@ def _template_file_default(self): return 'rst.tpl' output_mimetype = 'text/restructuredtext' - export_from_notebook = "rst" + export_from_notebook = "reST" @property def default_config(self): diff --git a/nbconvert/exporters/script.py b/nbconvert/exporters/script.py index 7ebc60bda..0875df60f 100644 --- a/nbconvert/exporters/script.py +++ b/nbconvert/exporters/script.py @@ -14,7 +14,7 @@ class ScriptExporter(TemplateExporter): # Caches of already looked-up and instantiated exporters for delegation: _exporters = Dict() _lang_exporters = Dict() - export_from_notebook = "script" + export_from_notebook = "Script" @default('template_file') def _template_file_default(self): diff --git a/nbconvert/exporters/slides.py b/nbconvert/exporters/slides.py index bd2c09b02..500eef854 100644 --- a/nbconvert/exporters/slides.py +++ b/nbconvert/exporters/slides.py @@ -75,7 +75,7 @@ def prepare(nb): class SlidesExporter(HTMLExporter): """Exports HTML slides with reveal.js""" - export_from_notebook = "slides" + export_from_notebook = "Reveal.js slides" reveal_url_prefix = Unicode( help="""The URL prefix for reveal.js (version 3.x). From 82485286ab450df406a52bbc4868c574459c0929 Mon Sep 17 00:00:00 2001 From: Tyler Makaro Date: Mon, 20 May 2019 15:24:35 -0700 Subject: [PATCH 327/671] correct some typos --- docs/source/external_exporters.rst | 2 +- docs/source/highlighting.rst | 2 +- nbconvert/exporters/exporter.py | 2 +- nbconvert/exporters/latex.py | 5 ++--- nbconvert/filters/citation.py | 2 +- nbconvert/preprocessors/execute.py | 6 +++--- nbconvert/preprocessors/svg2pdf.py | 2 +- 7 files changed, 10 insertions(+), 11 deletions(-) diff --git a/docs/source/external_exporters.rst b/docs/source/external_exporters.rst index f1111a3bf..3a0e7abe9 100644 --- a/docs/source/external_exporters.rst +++ b/docs/source/external_exporters.rst @@ -218,4 +218,4 @@ And the template file, that inherits from the html `full` template and prepend/a Assuming you install this package locally, or from PyPI, you can now use:: - jupyter nbconvert --to mypackage.MyEporter notebook.ipynb + jupyter nbconvert --to mypackage.MyExporter notebook.ipynb diff --git a/docs/source/highlighting.rst b/docs/source/highlighting.rst index 5ce807d60..b2e942fad 100644 --- a/docs/source/highlighting.rst +++ b/docs/source/highlighting.rst @@ -59,4 +59,4 @@ You can preview all the styles from an environment that can display html like ju Making your own styles ---------------------- To make your own style you must subclass ``pygments.styles.Style``, and then you must register your new style with Pygments using -their pluggin system. This is explained in detail in the `Pygments documentation `_. +their plugin system. This is explained in detail in the `Pygments documentation `_. diff --git a/nbconvert/exporters/exporter.py b/nbconvert/exporters/exporter.py index c18ad9617..56eccd4f4 100644 --- a/nbconvert/exporters/exporter.py +++ b/nbconvert/exporters/exporter.py @@ -255,7 +255,7 @@ def _init_preprocessors(self): """ self._preprocessors = [] - # Load default preprocessors (not necessarly enabled by default). + # Load default preprocessors (not necessarily enabled by default). for preprocessor in self.default_preprocessors: self.register_preprocessor(preprocessor) diff --git a/nbconvert/exporters/latex.py b/nbconvert/exporters/latex.py index 0c0ee179e..eae351308 100644 --- a/nbconvert/exporters/latex.py +++ b/nbconvert/exporters/latex.py @@ -15,9 +15,8 @@ class LatexExporter(TemplateExporter): """ Exports to a Latex template. Inherit from this class if your template is - LaTeX based and you need custom tranformers/filters. Inherit from it if - you are writing your own HTML template and need custom tranformers/filters. - If you don't need custom tranformers/filters, just change the + LaTeX based and you need custom transformers/filters. + If you don't need custom transformers/filters, just change the 'template_file' config option. Place your template in the special "/latex" subfolder of the "../templates" folder. """ diff --git a/nbconvert/filters/citation.py b/nbconvert/filters/citation.py index d86363615..c56997d13 100644 --- a/nbconvert/filters/citation.py +++ b/nbconvert/filters/citation.py @@ -28,7 +28,7 @@ def citation2latex(s): """Parse citations in Markdown cells. This looks for HTML tags having a data attribute names `data-cite` - and replaces it by the call to LaTeX cite command. The tranformation + and replaces it by the call to LaTeX cite command. The transformation looks like this: `(Granger, 2013)` diff --git a/nbconvert/preprocessors/execute.py b/nbconvert/preprocessors/execute.py index b6215f414..e5283a6e9 100644 --- a/nbconvert/preprocessors/execute.py +++ b/nbconvert/preprocessors/execute.py @@ -307,7 +307,7 @@ def setup_preprocessor(self, nb, resources, km=None, **kwargs): passing ``{'metadata': {'path': run_path}}`` sets the execution path to ``run_path``. km : KernerlManager (optional) - Optional kernel manaher. If none is provided, a kernel manager will + Optional kernel manager. If none is provided, a kernel manager will be created. Returns @@ -417,7 +417,7 @@ def preprocess_cell(self, cell, resources, cell_index): return cell, resources reply, outputs = self.run_cell(cell, cell_index) - # Backwards compatability for processes that wrap run_cell + # Backwards compatibility for processes that wrap run_cell cell.outputs = outputs cell_allows_errors = (self.allow_errors or "raises-exception" @@ -589,7 +589,7 @@ def run_cell(self, cell, cell_index=0): except CellExecutionComplete: more_output = False - # Return cell.outputs still for backwards compatability + # Return cell.outputs still for backwards compatibility return exec_reply, cell.outputs def process_message(self, msg, cell, cell_index): diff --git a/nbconvert/preprocessors/svg2pdf.py b/nbconvert/preprocessors/svg2pdf.py index 149d781d2..60b23b8b7 100644 --- a/nbconvert/preprocessors/svg2pdf.py +++ b/nbconvert/preprocessors/svg2pdf.py @@ -46,7 +46,7 @@ def _to_format_default(self): This string is a template, which will be formatted with the keys to_filename and from_filename. - The conversion call must read the SVG from {from_flename}, + The conversion call must read the SVG from {from_filename}, and write a PDF to {to_filename}. """).tag(config=True) From 9ba6a9a818337423648b1bad165a94a7da9267f5 Mon Sep 17 00:00:00 2001 From: Tyler Makaro Date: Wed, 22 May 2019 08:50:54 -0700 Subject: [PATCH 328/671] fix tests on windows --- nbconvert/preprocessors/tests/test_execute.py | 1 - nbconvert/tests/base.py | 14 +++++++++++--- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/nbconvert/preprocessors/tests/test_execute.py b/nbconvert/preprocessors/tests/test_execute.py index fcf9bfc98..87b7f56ad 100644 --- a/nbconvert/preprocessors/tests/test_execute.py +++ b/nbconvert/preprocessors/tests/test_execute.py @@ -24,7 +24,6 @@ from ...exporters.exporter import ResourcesDict import IPython -from mock import MagicMock from traitlets import TraitError from nbformat import NotebookNode from jupyter_client.kernelspec import KernelSpecManager diff --git a/nbconvert/tests/base.py b/nbconvert/tests/base.py index ee39dd2ca..1e45904dd 100644 --- a/nbconvert/tests/base.py +++ b/nbconvert/tests/base.py @@ -146,9 +146,17 @@ def nbconvert(self, parameters, ignore_return_code=False, stdin=None): ignore_return_code : optional bool (default False) Throw an OSError if the return code """ - if isinstance(parameters, string_types): - parameters = shlex.split(parameters) - cmd = [sys.executable, '-m', 'nbconvert'] + parameters + cmd = [sys.executable, '-m', 'nbconvert'] + if sys.platform == 'win32': + if isinstance(parameters, string_types): + cmd = ' '.join(cmd) + ' ' + parameters + else: + cmd = ' '.join(cmd + parameters) + elif isinstance(parameters, string_types): + parameters = shlex.split(parameters) + cmd += parameters + else: + cmd += parameters p = Popen(cmd, stdout=PIPE, stderr=PIPE, stdin=PIPE) stdout, stderr = p.communicate(input=stdin) if not (p.returncode == 0 or ignore_return_code): From e98651a510137c43e908f1bc6ca5098474285bfa Mon Sep 17 00:00:00 2001 From: Tyler Makaro Date: Wed, 22 May 2019 09:25:00 -0700 Subject: [PATCH 329/671] refactor --- nbconvert/tests/base.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/nbconvert/tests/base.py b/nbconvert/tests/base.py index 1e45904dd..57ea243f8 100644 --- a/nbconvert/tests/base.py +++ b/nbconvert/tests/base.py @@ -152,10 +152,9 @@ def nbconvert(self, parameters, ignore_return_code=False, stdin=None): cmd = ' '.join(cmd) + ' ' + parameters else: cmd = ' '.join(cmd + parameters) - elif isinstance(parameters, string_types): - parameters = shlex.split(parameters) - cmd += parameters else: + if isinstance(parameters, string_types): + parameters = shlex.split(parameters) cmd += parameters p = Popen(cmd, stdout=PIPE, stderr=PIPE, stdin=PIPE) stdout, stderr = p.communicate(input=stdin) From 32e424799fb251ca6352edc9db0d53a0c1cdbe97 Mon Sep 17 00:00:00 2001 From: Matthew Seal Date: Sat, 25 May 2019 11:44:07 -0700 Subject: [PATCH 330/671] Added instructions for bumping the version forward when releasing --- docs/source/development_release.rst | 9 +++++---- nbconvert/_version.py | 2 +- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/docs/source/development_release.rst b/docs/source/development_release.rst index 73b9feb4a..34c7ff70b 100644 --- a/docs/source/development_release.rst +++ b/docs/source/development_release.rst @@ -62,7 +62,7 @@ You can remove all non-tracked files with: This would ask you for confirmation before removing all untracked files. -Make sure the ``dist/`` folder is clean and avoid stale builds from +Make sure the ``dist/`` and ``build/`` folders are clean and avoid stale builds from previous attempts. Create the release @@ -70,7 +70,7 @@ Create the release #. Update the :doc:`changelog ` to account for all the PRs assigned to this milestone. -#. Update version number in ``notebook/_version.py`` and remove ``.dev`` from dev_info. +#. Update version number in ``notebook/_version.py`` and remove ``.dev`` from dev_info. Note that the version may already be on the dev version of the number you're releasing. #. Commit and tag the release with the current version number: @@ -112,8 +112,9 @@ Push directly on master, including --tags separately Return to development state --------------------------- -If all went well, change the ``notebook/_version.py`` back adding the - ``.dev`` suffix. +If all went well, change the ``notebook/_version.py`` back by adding the + ``.dev`` suffix and moving the version forward to the next patch + release number. Email googlegroup with update letter diff --git a/nbconvert/_version.py b/nbconvert/_version.py index a8d789249..4265e1d4a 100644 --- a/nbconvert/_version.py +++ b/nbconvert/_version.py @@ -1,4 +1,4 @@ -version_info = (5, 5, 0) +version_info = (5, 5, 1) pre_info = '' dev_info = '.dev' From becfb55b3f4d0a348034458de37969e0dcc805dd Mon Sep 17 00:00:00 2001 From: "Jessica B. Hamrick" Date: Tue, 28 May 2019 17:46:23 +0100 Subject: [PATCH 331/671] Fix selection of mimetype when converting to HTML --- nbconvert/filters/markdown_mistune.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nbconvert/filters/markdown_mistune.py b/nbconvert/filters/markdown_mistune.py index 841dfdda3..a2453934f 100644 --- a/nbconvert/filters/markdown_mistune.py +++ b/nbconvert/filters/markdown_mistune.py @@ -166,7 +166,7 @@ def image(self, src, title, text): if preferred_mime_type in attachment: break else: # otherwise we choose the first mimetype we can find - preferred_mime_types = attachment.keys()[0] + preferred_mime_type = list(attachment.keys())[0] mime_type = preferred_mime_type data = attachment[mime_type] src = 'data:' + mime_type + ';base64,' + data From 1dbe9495145fbecb5b71a202843f3ff1bbca930c Mon Sep 17 00:00:00 2001 From: imtsuki Date: Thu, 30 May 2019 17:07:01 +0800 Subject: [PATCH 332/671] fix latex exporting '?' for non-ascii title --- nbconvert/templates/latex/base.tplx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nbconvert/templates/latex/base.tplx b/nbconvert/templates/latex/base.tplx index dc512bb6c..74cd65e84 100644 --- a/nbconvert/templates/latex/base.tplx +++ b/nbconvert/templates/latex/base.tplx @@ -147,7 +147,7 @@ This template does not define a docclass, the inheriting class must define this. % Document title ((* block title -*)) ((*- set nb_title = nb.metadata.get('title', '') or resources['metadata']['name'] -*)) - \title{((( nb_title | ascii_only | escape_latex )))} + \title{((( nb_title | escape_latex )))} ((*- endblock title *)) ((* block date *))((* endblock date *)) ((* block author *)) From 461566741f4399deffaad53a28d2ef2af8928565 Mon Sep 17 00:00:00 2001 From: "Jessica B. Hamrick" Date: Thu, 30 May 2019 16:19:03 +0100 Subject: [PATCH 333/671] Set flag to not always stop kernel execution on errors --- nbconvert/preprocessors/execute.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/nbconvert/preprocessors/execute.py b/nbconvert/preprocessors/execute.py index 28f507a08..315f6e3bb 100644 --- a/nbconvert/preprocessors/execute.py +++ b/nbconvert/preprocessors/execute.py @@ -540,7 +540,8 @@ def _passed_deadline(self, deadline): return False def run_cell(self, cell, cell_index=0): - parent_msg_id = self.kc.execute(cell.source) + parent_msg_id = self.kc.execute( + cell.source, stop_on_error=not self.allow_errors) self.log.debug("Executing cell:\n%s", cell.source) exec_timeout = self._get_timeout(cell) deadline = None From ad6d34b563d1a137a19173c149b85fc11876e2d8 Mon Sep 17 00:00:00 2001 From: Aidan Feldman Date: Thu, 30 May 2019 16:57:26 -0400 Subject: [PATCH 334/671] simplify the function signature for preprocess() When [executing notebooks using the Python API interface](https://nbconvert.readthedocs.io/en/latest/execute_api.html#executing-notebooks-using-the-python-api-interface), I noticed that providing the `resources` to `preprocess()` is required, even if the user doesn't intend for anything to be overridden. This change makes that argument optional. --- nbconvert/preprocessors/execute.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/nbconvert/preprocessors/execute.py b/nbconvert/preprocessors/execute.py index 28f507a08..c5f1b9575 100644 --- a/nbconvert/preprocessors/execute.py +++ b/nbconvert/preprocessors/execute.py @@ -371,7 +371,7 @@ def setup_preprocessor(self, nb, resources, km=None, **kwargs): for attr in ['nb', 'km', 'kc']: delattr(self, attr) - def preprocess(self, nb, resources, km=None): + def preprocess(self, nb, resources=None, km=None): """ Preprocess notebook executing each code cell. @@ -381,7 +381,7 @@ def preprocess(self, nb, resources, km=None): ---------- nb : NotebookNode Notebook being executed. - resources : dictionary + resources : dictionary (optional) Additional resources used in the conversion process. For example, passing ``{'metadata': {'path': run_path}}`` sets the execution path to ``run_path``. @@ -397,6 +397,9 @@ def preprocess(self, nb, resources, km=None): Additional resources used in the conversion process. """ + if not resources: + resources = {} + with self.setup_preprocessor(nb, resources, km=km): self.log.info("Executing notebook with kernel: %s" % self.kernel_name) nb, resources = super(ExecutePreprocessor, self).preprocess(nb, resources) From 298f8113d12f3969c04d9510f0b943b991289527 Mon Sep 17 00:00:00 2001 From: Tyler Makaro Date: Sun, 9 Jun 2019 21:21:15 -0700 Subject: [PATCH 335/671] draft issue templates --- .github/ISSUE_TEMPLATE/bug_report.md | 16 ++++++++++++++++ .github/ISSUE_TEMPLATE/feature_request.md | 12 ++++++++++++ .github/ISSUE_TEMPLATE/question.md | 8 ++++++++ 3 files changed, 36 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/feature_request.md create mode 100644 .github/ISSUE_TEMPLATE/question.md diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 000000000..0fe004f44 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,16 @@ +--- +name: Bug report +about: Create a report to help us improve +--- + + + + + + + + + + +**Nbconvert version:** diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 000000000..1e758767d --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,12 @@ +--- +name: Feature Request +about: Suggest an idea for this project +--- + + + + + + + + diff --git a/.github/ISSUE_TEMPLATE/question.md b/.github/ISSUE_TEMPLATE/question.md new file mode 100644 index 000000000..7dc9af14d --- /dev/null +++ b/.github/ISSUE_TEMPLATE/question.md @@ -0,0 +1,8 @@ +--- +name: Question +about: If you need some help +labels: question +--- + +If you have a question, you can file an issue here or +preferably ask on [Jupyter Discourse](https://discourse.jupyter.org/). From db762689965c368063de7d25ae554d2571b35032 Mon Sep 17 00:00:00 2001 From: Tyler Makaro Date: Wed, 12 Jun 2019 19:35:27 -0700 Subject: [PATCH 336/671] redo configuration documentation --- docs/autogen_config.py | 23 ++++++++- docs/source/_static/exporter_inheritance.png | Bin 0 -> 36971 bytes .../_static/preprocessor_inheritance.png | Bin 0 -> 65726 bytes docs/source/_static/writer_inheritance.png | Bin 0 -> 8182 bytes docs/source/conf.py | 13 ++--- nbconvert/nbconvertapp.py | 45 ++++++++++++++++++ 6 files changed, 70 insertions(+), 11 deletions(-) create mode 100644 docs/source/_static/exporter_inheritance.png create mode 100644 docs/source/_static/preprocessor_inheritance.png create mode 100644 docs/source/_static/writer_inheritance.png diff --git a/docs/autogen_config.py b/docs/autogen_config.py index 4507ade7f..3db045b4c 100644 --- a/docs/autogen_config.py +++ b/docs/autogen_config.py @@ -20,6 +20,24 @@ Configuration options may be set in a file, ``~/.jupyter/jupyter_nbconvert_config.py``, or at the command line when starting nbconvert, i.e. ``jupyter nbconvert --Application.log_level=10``. + +The most specific setting will always be used. For example, the LatexExporter +and the HTMLExporter both inherit from TemplateExporter. With the following config + +.. code-block:: python + + c.TemplateExporter.exclude_input_prompt = False # The default + c.PDFExporter.exclude_input_prompt = True + +input prompts will not appear when converting to PDF, but they will appear when +exporting to HTML. + +CLI Flags and Aliases +--------------------- + +When using Nbconvert from the command line, a number of aliases and flags are +defined as shortcuts to configuration options for convience. + """ try: @@ -29,5 +47,8 @@ destination = os.path.join(indir, 'source/config_options.rst') with open(destination, 'w') as f: + app = NbConvertApp() f.write(header) - f.write(NbConvertApp().document_config_options()) + f.write(app.document_flag_help()) + f.write(app.document_alias_help()) + f.write(app.document_config_options()) diff --git a/docs/source/_static/exporter_inheritance.png b/docs/source/_static/exporter_inheritance.png new file mode 100644 index 0000000000000000000000000000000000000000..e9e6572578ac1e37047d9d8f7297e5922bcf9b4a GIT binary patch literal 36971 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 z z#*ryh6Awpczj*brnV{D^-U}Ib2TCR4UVD38+C0yMM~qc+NNo?l?k{mxoMO~xuFWg` z`PZXuzM-n#k6FGAJmi3Ec!6M&!wKz+x$TQbb3+r7^K5l0L))bO-%?{8@K37*HfaA; zWmymXGKwj5m~{OM2;@(5fXN}d-bK~T^w_lA!jZMj$KFbm(10~Q&={pk+UJJN5X>Z} z%amb7=8DOX4$82{mmOtj{)6FkEKjZf4o>7x0Q=B&z5Ke^akrA88qTEk)MlnmX8bZr zmMQOzp?C=uB^0WU@X)ESy;3!UlvUx|eOM7#)-6`JZlRXy#3MH{7NaM|u8P8;e+)aeZXJVGkcRl1_G8N>X z(jwaySHJPf=CXsn~`su}2xVgADzeWJC_{%HZ&aMHcZrETBiTEgV6 zcg{$&tH>&WvaxhH9>i1f+joBUf@@Eb$-}94j&}2K$g^ZHp(cL*!Cs<)6-TrWD#;Kb z%02z_Y~o07v#0)P)=9iOf>j{d*WAejYtI&h7S|4KZGE1WI98iaJ&B?)6_%e$+W(B=)V-U# z9w8xH9n*2B6veID&R~IzycM9|l%N(0;}@=CZO#`Z*u09`Ir&jJ>d(=`RV{&+*3x;{{p6%Zp^>I^F>Lj2(@fJB(hZqjO z18))X5g!;?&-op`ML;7hB{}U@No_Z{dc2S3;9Him50?(AOdqkIl{z z-JF`LNAL=$RsE+Jy$i9~eFXD&!B&7~`Y0{-NKu4ExhhxbkZn8$Qr=@KBjO{~ham6G z+Jei2qhUtYCU45%`8M!0oS93Cq)cTba>3z>BnJD$5+)NIFnP<@KvRE(6Mlu{z5gqEmehTq^6$sLQ$iaGqL zdPbFc>6xTUJ&n#p2TF)+qtb1hR#LmbO5ljPGie;cX@1z<=x6Aj$7aU{@IwtV zRo$P}hm|F%Z==eYi5)zu6St1DxWcGWeBOXKN5z@+oz3b|Gs!;UGpjZp+~HSA32OmC z*GwZ9+;i_F4?S-k@bpys5g|hBMNO8nmzMq zRHrjAVr~7a$u8zlt*S%j*~iT+yO^gkHV}Qm%`wKGMYy{cTxu>t8#&v?}Uoz3HmomqWVQ>e`sU1+wo%Zwzm%&?(6hOv8U zs`LeU?ECD4g>1$4)SH%QBd@;fa*^vZS(`7w5_$z#>gF&_!1FGjfOLA*dYoN*-Ij! zPr5e>f#mLCS(fL?O@ev3WhG_@dA>MrW_f;mzwq#4$Jwxua|PABR7%Y6z+o4K7Y6Nn1FIce$YD1IS*&F&1 zTQSRcVH+wWIh))x=8MCNY&LLCT-JExmK-WGr5D}6E4ZKvx9K05+m|e7!W)bwgN0wy zM6V{FNra?#*hPGrKiI+>LgW>x`T{O{S3fPucoybMj<$9;+=S}(MmJZa92PnVhmab_ zR>Bukr?9{*B#wSgG78GmH9Qp;Y^><&NxHIF#Wx*CIxZN zgx?T~M(usM$5kCBIgqE|%DGl>MbJS+2*FFKDoe_@ZONEwrc$ZRMKWZ%Bvx@**oTbY z%wId9S%Hgj-%WParS*Y+rs|4^klSbT@%3cXtLgg{UDOPPkgMn&<64H^5VU-UVq%Lx zS-K6CC%>x_5EJWQTx{R&J}V?c)DUkUm4&a3E%epzEl>^d5%0F=lyg#i1HNZ2d4E@~ zG`SeT+mTfr_P`7?$P0rD2dKvg$-S@Gn`>zIgF zH4$kHFA6x()nE8$rrkzvT~Pt(Ge)F0pA%Q&bcTJdEAl|=*1rD!2)ao_J-1ntkK(Sy zP>G5{RL)wtCuQDB*rq=_Rv2${Rr&F{S+4u*R)W!nOP@8wel9&&@aT?bS^3bq*@%N; z2KZZj=yWW&E@gsr%*tzFK$msJIEP`t6n*-oE#breB zoT?+PJ-eeq3KBKe;BT%TL~tGKH_~s<>VK!$#{WfY77!4K zFI-KQrjYjO=@k5am8ds2nmeKeYL%!UP^&EYp#T)$^A3jmw8iE4lZT`Fch+b!Q){E6c0Ls58_JE_%lCIsqHE3uu>Gb*t zj=7Qh8vw;VYtMB;H6KZ*Js=Q z47v`C`b+RLal^reX4v8VhI+RR5By_1SveIFPncJG-`c-!&qVlot682rRbZ6aR*tuQ zTL+5igA^dAs=gW%_Z@tTgyjc%Fs)Bv4O8~&#Ujd*1z~P_Rzg#smm3cFUs(#FB?dVe z6?lyOn*zF3WQ&wN+h1=n^$^vyAX) z{%t0m++|m|aNDDcliu|G!7wJm0-{!MFWVoWovmXlM9$adt zR7z`^9eUPva5~;GbU~2S`0PAIJN{0RFX?*yc^_H^EFcjXYt{ccGIwDV`>QFku|!mZbgopG=A3HyYa z;`?*x>2xkB;$q_R=azh!ygg^-Tl<8L?1}N+pV9HwCiGK1kCMaIwHFr!*7tuAv7YjA z-`#CA1&Zv%5g5j*jL$*I;_&GV2IWg7ymU$ZviGwZdI@-6yVN!NP5PpLKT z-{+FzI~*Cjv+|id_)gQ@FC*d>+3zu*WK9^C5)%_Veo1y8i82l>C^N`)&v=y3|5-%# z_Mb#|y)JC;e*;eE`i!tFl14{IeyTj|m>uq>x|}mH?(UM>m4+&Q-^)}+T#RePipoPA zE3g_vbBpy0(YPg7CZaj|nxR4M{{GF~rN;vur#IT1(tZ+uy7GW_STy!_MwK~ngFk-g z!AP99W1UfVN1SStNo>N-lvqKpv1v`f`oY~{T9Y0-512^T6+FiH*m>1H|oD z=gamCnp$t*9NyXG5@4-+CUKC1ct-(4wc*BUr4Ynyl-;1zi> zYC>WT4JP}2gCW-4h&JL4z79pi&lnmeFWm9QlolyA*jfb&%HKC9o0pvT| z*6}^h54V=J85#iM4yc9cil4=%_^a=GOita6c_Vqd7UVY?ZhVM6_CDPM9b^eQ=!mGt zLEfzoA45Vk5g*r@+4Me@GYQ@~_AEoY2LEysT@Sgmb|Mh*9GYMH^sj;|Qz3)4Gdup> z&kn^-cAvDhxom6InXx4m_r(0Y0zYkIRg)ZZWqL@@92c|}$OHLad3Dj|lYfw7NiFw{R*vRbfKmK+Kd?7weE9gb`|@ne zoUT7+qIjh9$ClFfs3Cxiim~FYgrjH7ac^|vF)gCft~HmfRCRms1WcD1&B(E8c9ZOV zbK~X*#NN6XLW?6RJM(6XxB-0Jiqil_4=%%IH9tIDf)GaY`*2INfDwoQVFaM{d&u?V z$uoE9Nd~p2-fMKXGK`pj*5m1HEMchtKqW2zKHDe8qU3qHE6-K<(dkX}Q!ve)gTSr~ z4bIQs5>Evz-*m^l7~7%3$#%ZOtsC@upH<#@e%wnw;m&(`r$x#VY&{Hc>}mjx-7tpP z{P3}Z8={`R6wPO~C`)p(v$7U7%K*zKFj{)(k@B1Lrs9 zsJ-eeqSPDvUN?tY>(S=x#V@w%t$Zh6F|f5s7y`v^JJo06ME~W%R(FP+ zvOc|LV0}sCXVy0n#O*cfU2X%LwR2Bdb&G|BCtuq|Ml$MdOe9ZiB2Lfl)e@S91jKq` zo{hCuLcWV|`paa}^y-$v^$b(^2} ztC^imV6`>rsRsEXbNo1?Nz<;itg+8h4vgGJECC{ul{DDfS$x;T#8}9Q!)6~M;V@2%xD>+7^H=CL?I8>x-tra}N=>#MjlB39pFek;A25`cQc#|1uLt~K$(pP>LGCzXsT^|$f zO?x``WK(M?wjHCsBbof=7~+?Ojh}nG&qM$>#`tBVdfa&8N2BMZ&YoZXDRo~~%o-q; z+;te$G?akE*xM2JhIZTI_(es-^+JqKbzi8-J%LBCb;*jU`UVA#}w z7xJgwoYW3YrFg+W9L-Sx%a%M{4-m_`)M;?}jf3key-5cnet(aDoGq%mQH+~RvCT<| zF55ncvq=8)Xwx328BHRHQ(;N`QofuMV}NUX{3Oc%!pHB^4LLA5Q9Y))+~_*apqX`d zwrh`Dmf9PF-%D5H?OVh%)vF{aDLVkKip%@_!0H$#nEReTw)+jySEd!u-$8Voh#_K-ew1C{aAd43Y^4m9dl&Y6 z)fV^D-ZSsLo(? zw(A=v#$rU3-KD!8pS+S?Nq27*=EtZL>sCnk(V10xACG(Q@yhh#h>P|RnxWUee_F0A zV+qe(HxC|qpHYHiv)Q!i($gI`CYJkuK|OfS&7fQ^?j2VPEx=oc_i)_(NL}IQucMZ0 zQSTc+NPUd>A&8aiHnCfUmZakQb@E|ZQ_7B|H;;ErPduUio9w}=$YSLgoz4UckeDco z9fVg~rkuCb>c000w=xQ^C+oce^TEQ&G6BFMUL%}^y-fp>#<*)~uDURZvE#=#s_&=Z zN-hPq;xh_C?H`H9ui#Yhg|Tb1cu0f2qs{Tocb0hyA$IOVSl5p;h85SQh5 z6fpT{2PQ#%lkbpw9-&oM4zPGv>0WC6*Zl;gZhtYmP}6_>1f;ZphzlZdc@)3plP4hS z1Xz#|9n7Hlnj;`}1$f7+%rD$y-)sQsGC;kAIAj9p8#Y`3$>U#iF%fznehCscfM^Rb z!~7`P@x@OdCHwym{r}?qzfzHMoBo%h4KkiKskKR&2afV51}GOQpD)W@+fTHgYVV0M z6*bj|Fym>wq;L#CRF2_B!KGhkUZ>LmElQw1I|vv|{22i3;t{VD(J(pG0u2`uy!Fv8r5^X19Yef0w1gD( zsdD%Y_mm{YMhQ+7SeQIIU@qh(v9gRZiR)grJ*1raG%`Rs>@jlNUH!)_n}~~$-=@Jq z#riB)%k8;FK^4Twn@8WwW_UX;L^*y8_=X490{{+}Gp+ewcne#_`5ifYWzr+~x3yhM(|$y3 z!qz2oeCeXA?Z}mczUKfHQ}SG(Q?Q_i#`pH}5x`eLBFt}lq`F4@>ZsAVV{7Ees^?Kv zDA45M%`~uC#nG<;2e<>n*3DiA{{tv001$Bic*+CT;9&Lcs5B`=SG)=(mAn0_LlEe0 zS3s0lgAnqG3pix=5VV-9Am)jgCSV68$6xmjg4T3A#CQ=85h#1?1gFJ8$2*54nJNdJam0|4^Rc89>o|J&if__UQVO~>P*3FQ7#vwc~JNEpe?%dC+cE7tgt|#Hd3el{fspa>L2@BeGrGP9DUz_MZ}lU)%1Saq zy@fR`?UaEW@G5@`C;z6-aKTUiR4rE~Dn4!XKCIetq398=e%-8OR|HH;bXy%%f#Y%3 z%?iJS*r0nr2nh^p*BMqG_PzG>d^PU~6@Qw=;fUv*Nr{{bojl!{Oa4>z@7_|no4NN_ z+qgFj3%b(WhX??gddwln+WKaw3ZQ!=nlLh>>nr1+dHd_GZdOrfbH!CcJdeC$qFPq? zewDX>M2WPB{T}c>*j^As`6^$0!7Qdwmg^&Z2C72vgiihnqfqajO{f?-bpE57mx^BI z(50z^44Uj@iB#FSUN7CW(kXt)u|n1G6@VK<8Ol4PXB0vALal9$_*1q)6vS>nCvD|F zlV7m-@?`|WTQ~sUd>J<(Fx`(MCvuw1zsHE|lf?*)Qs7D#ZACB03;IWrkD8576DZwQ7kE~Z-3MIWZdJ|IeyqAD9l2Kb$A!*p z!Ra17)fWg~zUsBPFLqZSXG}RdeVX8Rob#!d4zVNp8UhSgwD#7q_TWAlj#16>WDwUi zv_&867w;Uyvi02%suwM|D6grk9BNHEoI!0>Lu)@+oD2V&Hyej`=KJ|VNS zJ4(z{iz&2~;We~%#A?7x4NpDnPf*l4abi2SwyEHKMZw1)mYS*9yblsgz^JQDIBJo& z)CXuySPg^k`&Ci2!>}e4CVSbAVHf`_G$5gjxyWR)WntW`yntBWZG$Ao-6=(4rsx?x zULmMu73WtrMh&CSa%A10-B45e2co6o=H7mAbmebzDZKc)=usubc~jp#3WdLa<2@_) zBjxP)v$+Ghq|AIc16_nAyXJ&x znK%?+H@U^N4bx0MAfA~aPmuWs;Swz3`fQio^B4(e z3%&91dnei-7tF|y4rQSb^6-1*)V3&2Mclw7X}Sf`!a;bHtZQN&>VX^ITb zJ-w!)yYC3YZeO=B%@@Vn_eH_3!@<{T*~qPmr_Ha?`rVX7CCu(?pMVli=0gOM-#oR^ z?;xt^!rZ_rPfq%Y1BDNN7ZGczj4TC#wS@(TW!7)|)tjGYQZ1HY>HCt4)Pt9Wxr)xZ z`r&FNKZ7ZBp^k+Po%p5L%04XAR;H?=T0 zI2Lbq5RIhou&(&V(wEO7`#k1HweIdyjB?LNm<2_$;)^PUWR+k?8D_<{lKCcZHNRDG z1Z~mTnlvTNqECT4YwZ^*+4BM|e3_$|88}f8GSn1~{P45-8Y`l$EXxnm)}h-2-Dy#N z3ja4__&d*ApDB(akw2-(XOkaQeQct)QIefUI}tU=c!EoK5Z$M!DbY@8{I85Ws)P#P z3fXd0Ayf+t@4n>A$Qm8fyKPy}F+MMZqjodzcQ)Moo*TIt$+d8ikUsdtz9!nQ+*;i@=UPftn_4 zoHV4$*?$GrOMO?BrW5y)^QgXZ` zxX7kxM;7xC^#g~XBzEq0bHY;dzUoZu}`6J3~!7Zk-&BRS~!~L%HN7^g_@eut!$n^Fe zS}k$L!p>;f*IuWo`*+gANiDL#bF3g#!N_V^Q%soibT=hBL_9Y{&fPa3i?zE1_!Dvf z+SGw#fQ|?QP6d}-*8jNZN%rfQxH$AW-rme-p9d}kP({rp-rrRO|v>XmBQuqyTqMDn<6YhtM5OlTE ztK~?=>LTT>`l>hl^M7R|)nSvaAup3YBr8Kld2vKN-G(*Y5yLljOWGi*P!$jyK8 z`fUGA0@*?OHVxwqmvJ%7ViQh_X2Sieqg6M5pSUfo+k}6E)nyEdg9qA48&U+CAl+(O5B#*dVYJ<>PCFk|8|%6fj55yv`5%Mi?_cw}MJ40vq95f{ z@-VKEpI|3;G9GMRaC;u7hv$!ama!BO^y@d*iIn<^iOSfRovbkW%qA(>zyR|%qmnzc zkx~EYC&Z*Dx>qo&SAAUv&=EdvHOF1Qu{p0d7{9u`kprz@tIlu97r(}jO(4!{B?WcG z#2ocSDj%7D4{e7uf%$keIniFU%o8z37H6#b4NpbxPMR2l;g`04(iZol{~yo}OB_J; z`mv`A10yCWg6MqgRM7K4w5d2@bf+%*n*y=Iq-O2R$E!Ot5HE>K8>sai1|J|F(OKgGJ@Nzu)f?I%W&6=R+*jlvfmQ!xHSAEMiAuT~3nHLJ2Z0aOoM2IkGVWoT@)$5?muF*jBp0};i$oT3knS-YDEDu#!L=YeuW zgHincudkV4fW*1A9j!xSdkD)uBjp`9Qh`I=0JLir^(i!;NiotHXcdiJoh(B7>Oc{z z>|HczUElz;7xwxe(42j3EWeaRn4jz#8Q!A)6?#THL?zO9C854!kLK-jTdX5^$;V^h zYv}d|AU5^flHr;8X}y-a*!eLqH9tBx0;yuWo#baB>ZP{URbdQ5<7fg5XZ5doOt?M9 zvGtu7h_$5En?Tg)xi#lqvD5cLOmX$jwwRwJtJ`xfK&cO)O5KU@=Ap5B-V>9PnR$pT zz_6rrgQhZQRdNpCyFM6K#XAM_(7FZ{#4z1ZL9BIEEzUvDRm9YH?1wFz1%F|8W#nhR zU^4SS+UmPOte<`INhB@^GgWND9+94Mw>bKEcb|oFUi>x^P_Lz=vHrf3K6I!2*2O(g zo$C>(bNL}boeNVVHeZ<+!xt`P<*BQj;myi9zX04^gCU*Ss&O4b7ODmhW2B(?x4QKQ zF$WwlM$!R!jR3v~3R}I+_mzv{R;scatTXY9xjw!49QYe#PxDYvT1nKZQ(kAMv zS+OCKE}Na+Z+73Wd_wn$p@=?0$mV97HkqjJ{y3*dXUOL!Gr@33dttVxBG=_f^=k|x z3|BqG>fqb0yx+8U@RQ>(9qA_QmbX|g0?-6Rc`Y7{GSb*8CngcNDSwViu>TDn5$FzCye$S|Kr7v0b;b>^5(R zjqO-br#fp|l!9j~ge1u-brYsl7)Y6p`?-8o@gR82M~aDkgZs)1n}mvxIGVo(rmmS1b~5UJet@1h2@S5Cn7r=!dLTz!RKrouHmM<|B_9%~Gxe|f4v*E@ zoNbi00|6@DG7&YKRoiZNc&l`Rajo&<938Lf0A#M!zl6yApO4&*&nCt58PfOeJ- zY7I6{Y*X#K-ue?^89q%%H?JJnd_9Y?JUx4p5$$TPag?v9Ge*3bcbv}MNix3R_b(NU zRWI~msz3iUbCk)Ko{F65bX)bHhoExACF6iRrBbN9(f^mOcj03-)P9|;&ZJ>N(2X^l z7u$a)J?o4~?)aL%>LlOAyNP-l190-(uly11xe;Kn%7sm&5#`gAWBpw}``?FR@l8l; z`WAoA0xXZ+ShV?C#SAxaUI zULypEG%10E03kpKx!?G{`*-$p?{n_?>-+(M$;_J7W@gP=pNTQl*X3ltzzza|IPcxP zZ43f2$AUmd-?APDTI7TWG=PtzE}D9pAke1-jsyE+!2fJMcP;%vppzYpAEs{aVn?9y zT!4;6fQh$rK(M`^6UfKj%_~6K%PHXEP3h~>ax{ImWe|uSfA6-YX$WME#`e=jCzC=C zv!&yRp>WxOr~SnP#q-N2=H+Kx|Dh1Gt4z(Q<-pZhi@<5b_k@Yk=jvXd8(Zxi^36jX2@l$Wc7ji9gry-g zkk&QMuc_-u@0$@nL5@pxeG=2}XCtS_E3A`{Sv1(uK{aRLK|D9N9ll8Zv}@a=qdFJ6 zd$n>fVxp7k?x5;BhDSGhB#%M`HBY+=zW6KAj?tSnRs(;E`}x5Qj^_|v!=qNwIjjq* zmaN-KFa8=-0tlcP1Wt259~Ty7-gDw*M#bl_)}GWn{p}CWeJ5fs#S1RLcQcV60#Q-^ zO#k$%e3PRvUwd{+nZB4E_tJiOLzCL98iZ?dO>?${O&5ay3S&+tDKESi#+ORhFY&xT!Fmq5Ae2Lxi|g(X&N748)oG#t-#AYy4~$ zN16OM%hMsHKo#%(?g-J*vMEYN)Evk)GdPoPJrY8lKZ)THYJpJwjKb`e5+u~xs`N?* z?go+$yR*@q_>&a9b|w5&G`I%hU2rpJX{QJmMjIQ82!h>_%JfNAw@rK48mjsz|UFvE>ozpzo4Ve)? zfGa4Y*YXb zuBk+u8IJDMs-fTk
ob@XC3JAMUCpR6V15DCdU>{LHbyQS|)UDXULITiY3wyhc| zS1a8(`%Qmb{rN0^*rBX}@~5hDNcWPoN=h(lzCvfoagajMr%Yh*vLj3D_&TZeXgdlE zjzUBzg%TrLYpTlCI;v*CVdr=Q*Cx@*$h6G*p``_UspfyzgsrNkk?`Xg%lzJELjeo5 zoi~&M^(lLPS}~DQH_btu2hW^F(nF{ec42mRc0YS%FgMxiiw-Jw|GD|!#v!jF=VEke z*QV>aBK*iYF5J-R+tkX%gD~WeBQEOT0qhR@ z`u@{)|H^BH5h1%2XhrqL)ZVGGoT9~!twK*c__$iHo4QwPxK*y2ik&WPTiLB@JMauV zWq&QTKif0}xxPG=rFz(?l@_l$gSPsg^<`jK%lzuf3CVq7OhO~{-KfNZ*yoohC3ikd zG?RhwQ9>$!gi1^EQMB^nwUh$cVKP2rAALF*;o0g<*lW7ki=Y-djMR^H6v?p%9ePWy zxpC9xuiDH+^yej(kyUCpH}jlETT0jWDefkHIJnq9TiDoH?lJ3yNNq)X1@^E{2ZLEl zVS(JwwmZNlrNn=YUf|}j8QuC=Ra;4%t9?j4BVB~C3T^~v5v8hMJh?@6MZ|2_{LQSV z&?`pr8;5dB&*xfJNH7GmZ1e)3DKItMR((F}N7^(qeCJ~a2q11^Yw=rMe%+*mwi$4^ z-DE{w3KJWn7E=zl%r9=I9xIME4BY1*4cb;Lg#^yRch~P7G$Fbzx&E~oU1&3r`S7R+ zazXv!`NMDG;bJd~G2&0b!y6^c`UyE5uAvE5E*{#YB zD`GahEeaubGi3x5Tvgj#Zpb-*6V}l_SafZ)%r#sRhuq0E4rIqpWkJ_dO&O0f)t6nw_;0P8hzk2|1xM4GghK^dm|%XC3azP%`OVIylfvwVvvtv% zhyi%3=+hM&EA0Vbl1y1o*^KvpP0E80D{_j3;hFMa$RDRs;+kDT&KPCN&>>&+oeojE zXfEf?`i1qMHt=#x7E_pndH+z4Xg`KSQyUN^ZZlKugU(T}l z$xbSeB|MCa;!WSLe<6=vc}=L^(wEw(bIta zVBEucQU)E**b|%Ll@AUA2?1^4{D4EY!pCR>Bj|^8Li;?0T$)6=BCG~%#yPl#>)P-S zhp0WJ_2|RnwHd4AQ2Z8!ydT~U1rF_LMqprz(lpxcCaIA z`;9nK{gcDSjsbCQaQ3ben7gAn#JWK3(x-T3ulWH4A5y(M4qrEI97uv*Ir01q+{+C0 zqy1lIxOIkmC@kEtcGd9wKLN*4ozWLLKhOQMPFQ1$5$J%=rP@yanC^cYsvOnH3H;}j zyd=t8q5Xe~h8le5?@;UvX6z4(nZEJ()zC5w@&D0bk4g`MSCwZhCkxsdvyk;e3*Ab6Z=R zIG8RX z3-uNAEAL4?e8Ti>lEIF-$9TE#ysn@_)EDE?BPG^z#kw~jU{KCY1~(6rtOzYDTC&_$ zH)EWBZ_6g>tdp5QPZ^?5R6F%dwYoX<8J-)F@-2)9_h04(75`CcA)DBkzWY6qPZ>AF zEqft+|0SlMWL4VqC>|;lXq_1n3es?5kjcDSw=5=Mrm!uCv|~_keFg}WsiFO2@oJsx znVy5Dnxw*=P0+K=UM&!-Y8XX_&}D~9Y^}v(oxbmxOevmShv?`%IWNC$*sysgDw8P( z0#NM~f&{#@jv!9oz7dvB&ICm<88UsTaSZ?M(yn6Q1qgXzB=PTmlyD=zZg z{-x>0Uk96gUY0~77?{~3`9Rka0Y&;YZ zx4lkTU|P>MXgye%{w}`04g%H7axi_~Z0ROSvd2MW!_L!pH=QX{C_XOMa0qTOXYyE%P;${ zMM7K;skPsB=b)o{zMAGB4Y%&timjDMGbJJ;x_A}Ek7S%7d=hgFgg8^A?r(zp_U6ifOL5b_Zhsqr#24kYq=MzQaqT!!2 zHD)9B&qqJ|XfUrc6lEQCm|+1wCe{F**;D(DO1v z@%&cNGyK&4SdmLgZOSHf`GQW?Bs1s;aN&?QyvU{<>%}YBph@HA?bx!8*K(?8=n<1o z1A^nampX_}zm9&W)q_?T2&8%1R`TSH#j6H6a$~(AX2e+&n9FFOhk%&;!J2AN-(b6x zpCtcpVp6>r|CuC>ALJO&V}=~3`a{fIWAH{`)zE^e%ehb}*Mrdgc()@jGIc1Xqf-7t zO>;lrwdZ<8n@C@bU2RZeH_q zeiftqy9doWUR$e8&KI}2Ph_K9G$-Y_sGOk8uaZZVY{LZwWwZE`C^@vvBuZl{yhbVW zeIEVrFpW&$vKG=WUE2XYTYZnvDB{r0({ixk0)fN<#po9bf4wH^eoo2j$bTDn)tiaH z#-JCwUJv@SH!V9h^XQ-BPW#3T(trUM`6SY^8VFPkWH2%%Gf2JpR1l~*8(>&mXqpA2 z5d`!wS%G$56TR`~80gtPVBVs+A;55s-2j(734|k{i^@RGpd=&!3<6DD2Uy^Vx_z}8 z1R6XCIM8jCW-idPr@$zYSdr>$^XEa&z5+~({D%Y(UnrgaL&6#$5%V7sK+-|~tbt?b z+7ID-pQd?1AXCOnoPWB70o~mG(~Ti4quXLy;J>=*9W1vs3V}eQ{lFN#b7w;TaCHsn zn`i&~n&|3RtS>(ZWC)x-%a=*FQ(TB`qGI(}W3Z{K^B;l6Y^-sMn}ZmwX5C_#LRl*hEgtr6l}7heT7Z#`b97$!ujCz z-3r1Be|_Jrr9Qnj@g0iD1KIwpHQsF4D{2R-W-430*FR$78hBZ89~-epI8S&nlHj+s z)UV%mVTV${>nxn;5*iwi9o5jEe&Z`@9DQTY*M_V_jXt|9g>BkCdOe}7x!XzM~t6(QC)Eu0}Ka!!}x1}w}G zag(bCJ`uZp>t7Qtfr!a^4RSQAF0rJq&E|v&xrGyt({VU|ud9D_U8bViY@kURplx4e zUR+ilj?LNqK+)z1=|26B0&K!`gTne%tn+Xh_<5Erikm;IfKqIpHjj5S{G|5qH)@IQ?U zD?>1?e-A3srLg^r=zWOL^wKR^QGJ2Xm&&U}xLf#V`u|LDCaN>Y!12wU7jt_f`hM9z zNi?1I$055puxX(dqD}3?8cS*bPLD6mQ(~wZV;huWi4mMjetqFlsVvBRdGx1ipkZru^LKc~wgFdw+YDTmFg1Ef68BJa=5<*I>0& z9Gx1`)MD1lZ`iOxaPsQ;iXXw48MYo)*G!89?+-9nNR)Ho9kWqE5~b#0$q4+A#~u|; zO}~6uKY&i|+;K7Yu+u*eeIM41Jw%4N_l>ztd`OqMpSnfVK|U%}LSf{%7lJ}CXzu!a zKlJpjLVKW|K;TDZr3-!(Y6WI8YP^h0U95YxTJ9gTV9=HsrGq+28Ij@ldVqvAy`(q|ofrqfxlk2)a)zl);)~c5{JJok5`&Yuy z;UR79fx9(({KEKg=H{9!TIfnEykkrJ1 zpLak8%m9hK+XLT5@~N%R7jyk!dJdN8(UKmRtFXPSYsGYrX~g6iN5^M$E4fdH%*YiZ zPF;akBtqK(7OXZ$iuKQtHMDg%$5``fX zR~qvDaA(hX*)2Um%B&k(r1I?4;*@w%@RcF!VY0g_T#)_eV#*_H)DCs;Fc@7{wf0q6 z#sJTq0dJ*aI2I?rM$@;{+3_XLxCfbbh})k2;|ul(KUk2x#i$3YjUC^n7b3~-GP`&1 zrOi%^J&fIpy{ip^9ft1)Mr>;nrAEKA%uu}%PIWkYl={v!$@QRDeu&HPqHC_ufp6^pa-M8g=N-@5h*{r#x!ESegsMqbJJ>|iJ(OC_MhH)TZL4>_|ILMNJ3V~) zz-~S_(?A`L5Mr=1OJ&FyTq16>Z*a)KRau)GSK5g1syUDtLt&qG7J9ma#eAJ_; z-hSe``hnjp3cB468;u~(P_|c4HU!?GA<-W7TcChD#=%qYqZFu2f|a>N1WhHw(S($*n~TtI%FQAgxYG zh(=Qj=6A9YY7SDw0Ypa7K`;l_Y#Fn3X57OPjoi24fxt2+_dNGJqqAEe(O*!bp=HN3 zPpe)sNFZzh^_czPZl)h+qJ$7k$C$|X_i{CjSlPp(Mx`scj+hBFs(9*R<9bk4E;r(G z&ZUTj6h9?R#jH^8MCP?8sl&;!Vwcrw2p?)^CJoZmeD6Zx9sVNXTTD$KU^0Plp7HLi z5LcrKy||(xJjUg>x*a3IObnlob9>K0(d#bsL^cO`P2Ty^Plt7O{9yk&yQyXqt5rF^ z^-D%Z!s-s)XE2BsR#)}jjT^sUe!PT@em8J$AXdo1ao!oq zAB)4+1}I`j;hAB_P*rLlw>~j>s(HdE)aJ8Dv!*2>B-L-%l>e7k0fk#|CmmNUG{@Uo7V7KRLJT<>BB&K%MhUl>#0Rsxk5S~ z^l+I1D3}ac54YICnsVi?Zd3Btmx;H@r8Q~>G3wD93iu&6O(XSPcFEbqV(6#Q{)T%4 z_cpZ&xHukeat9HUi&@R(NR7e14Ci_xrFmVlX6O1z@G4>?Ot6NyxT)%YLC8E)%dXYi zKA~(!Z%L$_D-87Dn9zC|bhmwRZn5IjIpl!*JcU-z5zUhvy)>+Z6qW)Xa=tW03Te?H zo^{d)E!?Hzh*QP-OJ7;;tCvIEvEn9~kW?k_Iv{KkSy&%}L4Z1u_OsQMn$^*f`DmpM zushjC>|61Y#8dDl=6qB~lCO^#k0bZRcKhDM9bLL^U8T3?K19_33O>#uvAhfukSwn% z@QlKn4_IH)ybh5w6;E{gHclZR7IWQqP@{WeE|s&ms)wk={fFdyi4f=N;U2&Fu3Xp>wX?{sWzS_UM%s%@W>@LpqFD|3 zv~{sRCtbJjJZSR>QINz8wnF!}l?Z9C-1rZnUH8Ku*Y>9}qgT0=VBVGVsg9sLz?W_T z(W3DT5G4Y&7O(;Q8@Y=q|8KGirXF-r%E}p;EWw9=t7# zW&WGlOyPu-yPcllHs+%n)v8DaqekmS)8dpjyK(v*uYQCVvmx!2*K%OP)z_Y(RM|8| z4i2IblqU!&sWMM0c|e`|(KDA!rVU2S&kvreU=&+Rj()S2zoSPa z+o8;xye~Z%P!pdPSw};n0}mBoGHI@=x`K_WYvcUf=5|5G7hcUd)p?-@d|7Mbzp%&V z!RQGQ)9{&q6SLRV=sh(f{$MXVJcU@tSx3o^Y#`!0zRkKUtc3hVt4$HHq3n&5SrF=g z>Qp_Xtd~-FhWojA*I7$QsI#r{(+1B1=!?8!${qE7_P8vq_NF2(scqJ;xw_gpBH1DD zsRNV)Yy0!ZT51ebD-Ds4PgR=C=6}c$AW_TqiT`5r=N!QajQ*;6$kjKk6jD!od)TrQC7krah;?6H1iX>#I{`=CU^E>}PqN z#i;f|B;PT^v9cwZN)C$D;X`%W8^KuS;8ssyC|j&XGH_w+BCi{J8*Wnl<&fNts)u?vVOG7!JDc)rx=<=ERwDR<(FFYI6T^OdotA$ zzZL$gI{x0=mPlJA;Pb)o`K{b~Uw+_xn;dl83a5qOdxoT*-Rh9AIsAQ3>mE~a z6IV9O`RLmQ_=cTFbAg@sAwSX{$KGOvQVd2=C{injInV+Oj-15*?6_e-<&iG)2R95I zMtioCsRQB*31g)ksjt;d@CQ-Yc)UbrgFBFfJKLW4K3bO&`ub=WWT7MFQEG5zD={>Y zl4HRIRMt?g7phN0-+J@f7=>Wmb!`ngdhHEIUpLIlt!o*Ub83DH4sok7F7p8XEeaGJh zrwfHyqN1;GT%vK5sP&2a$d(F5S{syuF zo>^A@uK|>h-e&SAw63MarvrD&s8j)1m}NV!$Sdu`_uv7^qnj5`#4zsA4Q?Y><-PYy zekWXd)-o(aapD{Bq0wmH&A%QBIceg$nu@?KhZfAJyQ*tF&e<7%#!gBNBoH`kBc=nr zCJ-*oOG1tP3}%Kr2Fe4_VcWdHU0q9!Wc&wg`ZCa|&THjWE}==e|3-wubI3-~<%_Fq zn&!Pi^W#XA{GR(yT&fk2accomSR>R%JxIBAShjWeijG(R_Pv;xZK83a!hNZ;n((Jf8x@eEgF798bZDZ$Wvd&(h6W(!{_ z&zuM&L&-7N>6b&@O|SXouclWua6C@mR#rIBb>=cd9r_R@<>AW)dnbcHHK$w~LjHO% zf*& zgzy%@@%GUqmg#Z2-+sw|+c#can!dtk_l{Aw+w77&Iw=MZ0H3wIc%kru`lhSw!6S*x z&Ku6Ge1k^<@1i5SX`fF-jNKRcbV3>6kOjyG|A#}h52Jax4iZPr<^lZ1)GIfWq(>8Y z7Q-x{@U{Gt{rOm<(;=JJ*R->YX5B-;0hAH4S+`2ARO%Al1B`p#`HJ{3sSZB7BU1P9 zJn3v(q#%$apVUD`+j~C&R%GSGl*d|%M?__p#SUsPF|1jEqIi=qb`2LJ~vw>{Ryxg+q z|EF3ge;ERPHu(|vWj@07pAzDR;F=1cPifL^UT2k#yn9lV>44nx>{1^(ZvaWn?|D1> zpwT-(Y0*~Y>j^|kz?q(jBt~UXm;s`*1@gG1pPo+KV4wh~yVP~Avy)#N>?mCGsDMXtt8 z;I-O-EU6VL^Zq z_gs!u@;e?00cbi##^nsh;N_K%DfZE6vx8y3$4eeQS@8kHJo(?^!HfGhKiYU1$6fu@ zo2{(u<}SmPbNzy`w0wz9lEv?V-(XrMP!*eeu1i@=%|d5(Tk}XMM#L(+AtV73O<84Kd^T;;!c9C;>QBiD=hQeqT4cuo>1U>#S$mN=xIW1lL(h9J$!Cags5-k z=FRf0Y+L#LQJ{!gmPCBa5q5s$S%}@H;6p7t*Ui7R`|e5kf2EDuUKiOrdFM|h^(gL( zK2Ue5D8!%cIMseTB|KKWXk*LWjH?@LH?x-2LYZTFK7eltuU1~{)Q;84yZWiPi1gh0 z_lKTuzaK4KDQjq8TNoqCUBP|1C<*Obb2^xR@I82gu%LYIm>QB;{hhGj&}RNWCD?c2 zEU%p$w=%<7l%HF&)?P>;-~~u^ABD$OLZ?YL$om~B(MhM2I!-`yNgXG}K8#ie%-oh= z+Me&N&n@y+mza2k^1O(Yv&{y`REd^`Ir&H7zAPmXe+hmM9%KauX~Ru#mNA=0DOBaB*4xk2AE#AW4JjD+8Tc!PQ~*$ zS8Z{h!u0RHXmR<`J@sQG$gew`o&3^q;4vd#45iQ2EN;4+5mUn6f7SE zYQ@*vB13_LrGoc(oTNzgGX3F@0BMmqc~ohyYv5^panVLwO|V{Mj{IUGr6|d&UjSB| z9@rTcyE1^M zF+d-+#0oIMBlay58o72@iCX*rDprU902DwMb7AYz_PCPbyA=2 z0ISq2HmtBZgH%qv9)-WtCi=}<`^o=LN!r_H_tF*%&$9D}gC}qn@cp*PJMWW<_TLg8 zb2FpPqPNuKNzq-X7-7UikIXzQ@t--|$MdN-sd6?lo7zke?V*Co^wQCMSA@n;VhxnH zLW!(ph2N6PKqL3G*#q)9mqc)1%6h_CB(_DZgbi;llxOLb>7x?I_gl>yR6aa~{%XrX z0q&7JH#_UsAzb%&m^jbFS}8~ECE0)K+z)69p`{J&8AWX3X6GcAa=W6_@o$&LZC2%` z69q72jU~gu8m-5Q?0-WA9zViqUg{0~kn;rb#ZRbNa_{5?{2AN)Zzk580HVdJ#4>?k z@}5nbyy>9h1X9@1CN*nu7%9@Jf7oH?8$XA(nV8_ zykzn5GrT-!dN`HwZc-%R&J65L;``24);E zC#0tS>aMmwRC7zO7QW8*5vuaI6?So;G>ud~(^2H*`AgZtd3Pdg?Z<-h?mnZw?hI(^ zquxZ~`=iE{2E71?+cnu)wHcYlREt@eG(ddbIEY$ZQI%X=Q7+_U4dR_PF>2@yU5Irs zVUKPW)NgtwT8i6WuJ~TKwtgh^qX<&;sMW0De+FRR5_3)yo-`Nbd8KWA1NP$gh6@(QV1{-`fnJuXBJc5M(=VwZ z*dUN2m9Su-;jkyTN*35hB|7Ch$4^W&>Zt}~ zIy6Em4W8P}L=)`bn?jUfZ?mGaCfhiidd&>YzNh0}^0d(M&Glu~@~$3()kDJ zN$vE13jY!>={6SIUPM?pClzy>v)@?HINR$7$?K!YMc z)bfL`$(r-ScqZWKjZUq_c#{4c{%j5O2T*+n@`H%ck<`FqkFU29fM+cHfLf!RRhp6K z<%z(H9RN0`A=_D#G{%3fp@fH__(4V>d7|%qra)x^Lx^uFfDqc_s0j8Cv-{QOelP|B zJjlMxm5!T1?`FNSJmGW%^lTldE_;wXpWZwn*ie!zG{#q%^v`I=it`Ph*f6PGs^7*b)RQS-f}22KU7bP(mgBDyp+6RIePzk& zSOO@<V?d2XMjc0)p-t34q%OPM^)10Hnr< zEBMFV4BOPk|M>^7Fbs7GV7m{P{|p5rcV?}T;hf{Z?B$Mo%WdV}QvfqR09ulrs?UJ1 z?J|JwFuR1ee+qy;07r5Z#tUiuVX+Qik;4+WQrqzT(R?@n7C?mj)gv)b{s4f=BzK~c zE=0v+9V^giT!mHL4>Fm|I zk>JwT>}%ip&iViS2PARnwa|fsJbcZkC}O)aVyO>!0cFV=X`8RzWjW(q^fql|ARz9y zz8~^w^BApwF1?g>Y*2DP9<}{b%Ik33Wa=Q)KM9#bLp^<|HTTLSf&lj)IZvqL+?vV; zgOp76De1e5FYLXxV|432%Fjq1y-)yPV}9OJGA*~p?*`t2rf2$XNBR-o4H4@4f}7FH zx@%1@4>@uaA@q3KN?J0ZJm%7e8|6XcFJ}o2WraoRRRH4aaVvbs*K2hl=6iM{EjXJ8 zaCK9aH#*wgzUl5=xiv#eI;sBOyZD;zkTw7?!b)nde>!;U#rB2r-LMH(a^qCM7uT(B z-et!D`sP#l{g=ND)^2h^Yqa)p7I0EF#36h1O&Dx(g4@EapunB+uw?|eAO zoVgY;J`k86ebDPU-F$1$q=9}|11RkEa9>@A59Zq~1c@Be*E&@HV$OqB)(gH4>MI~Z zWM9lhATu2p!{c-Qj^}}x6i*EJ!lbIhPdvWDKvSH1Htu@IuonLKxfcM_)BUkI1hZ5> zUUdd|0tErGInbjz0hg!eubl{ftk)}gRG z2E^hoQRcBrqIs8`LjBhyvkAtW|DyA;yxkVqWbzGyOO69*4Vf2z8LGbcODP`8Rl^=^ z3Bg-P{i}0^BzQV?QLm*;=U!fSUb1Ix`WX_CgBWTzo^lXc$=PVPoc z$RY1=9)41GnM-6^B>9HM{eC4gO2uSqbX%@k3_?QLVkvft{p;i=F0LHEv4n#O%(TG{ zb?Vt-rwkqo9nlq1cnnK5P|YiTJ=5)3YMB5e8+}plChd}~*J(;+6G99`Nqo{Hna3&G zKkJHtEPKe>b}pq+(Il$HgEzy-r!xUPZN%i5FdZBe%{3XUu?OHT%Fg*MaK**z30a~p zS8&+{GYc*O{1*5K>`%>52*GD~{V#H9ej*;6oi0+Zj!akBZ6U0l@8&(RM>3!OuDhO8 zpQcRcCLrl(`mKwd-^;H@PFdr&xx9KZu|k3j%*Kchxk3hB>k6weQ2U7@;$}WX>7&;U z!-yFv(b&;8SHZTvfwqPg?206s2|A2vlSesRR8)7@Vb47*4 z_u}|cJ%%sXw<>mAi-XEiP77tPNB}81^(qW!q z;jf)!TXl;NnaDQsc}jl+&14p~pQ%c#%9FEdn1n&Q>7wH1h8YUG$~sQD_pv5_Plq)m zd}K1EC%;jr-pQ>1Uca$v6Z1-Ro3pPjS<^YOyZcpr6?_>M#TbtED-F&;F5)2d#GH1%X zQVqh3s{|(iu#jlEHh{LlPE%)S((;tvHwZbrH%dYEG;{_PD5?{GJZBA^@(XzKF1k=!mW%NZav}?#d z#Qs;G_kp$iO5?C*Z}foiFj42!e)+G$oT4BA!1Ic-5gX%{*`6c=JVK1ZhAlMTTk$w% zc+8HJYw<;QX(W|vkCf#_G4ND(u}bJqJ;Y;0O{1TGZ#WNM#;|*G=kTzP?X-T19KJy_ zCw5d7=j^maM0nUM8Vyt;%@sz}_L^yJo1{EX54#R1YKN{csCY0M9V|L}Z#OvKGn#z( z6ZQ}CLpR3TC7&wx)w8r9_qUJje-|$JaDaX{;%P;5B69)U0+s_HAJkgojnv!O-3^ea zX2U{CBo_`{^-8N-Oq)w$)b1KAz{rrcmw-+p7@V`w1=B~gywgNaxnb2fl+j-A9+y*AyF&!d^1+9q~o z$BpQMZ~gKgtFL!8{aQ(2qmc^2eRgPL?!8P>@O}& z(*8tu(U(rj=3NyV3HC^LzVXcP(Ka^=66!^njIx-jNU+QWTUQef1g8}8@j}nsufkus z*FHkj>=NF`r%iAP-c0gAT#RX=lzz^zO

@?XJnm*e=-S#ywYE&cZ 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 HG39>&EDpZ$ literal 0 HcmV?d00001 diff --git a/docs/source/conf.py b/docs/source/conf.py index bdb3ac188..7693c28ce 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -20,16 +20,9 @@ # documentation root, use os.path.abspath to make it absolute, like shown here. #sys.path.insert(0, os.path.abspath('.')) -if os.environ.get('READTHEDOCS', ''): - # RTD doesn't use the repo's Makefile to build docs. We run - # autogen_config.py to create the config docs (i.e. Configuration Options - # page). - import subprocess - - # subprocess.run([sys.executable,'-m','pip','install','-e','../../.']) - - with open('../autogen_config.py') as f: - exec(compile(f.read(), 'autogen_config.py', 'exec'), {}) +# Automatically generate the config_options.rst +with open('../autogen_config.py') as f: + exec(compile(f.read(), 'autogen_config.py', 'exec'), {}) # -- General configuration ------------------------------------------------ diff --git a/nbconvert/nbconvertapp.py b/nbconvert/nbconvertapp.py index f70e99a6d..e0302cc66 100755 --- a/nbconvert/nbconvertapp.py +++ b/nbconvert/nbconvertapp.py @@ -13,6 +13,7 @@ import sys import os import glob +from textwrap import fill, indent, dedent from jupyter_core.application import JupyterApp, base_aliases, base_flags from traitlets.config import catch_config_error, Configurable @@ -510,6 +511,50 @@ def convert_notebooks(self): input_buffer = unicode_stdin_stream() # default name when conversion from stdin self.convert_single_notebook("notebook.ipynb", input_buffer=input_buffer) + + def document_flag_help(self): + """ + Return a string containing descriptions of all the flags. + """ + flags = "The following flags are defined:\n\n" + for flag, (cfg, fhelp) in NbConvertApp().flags.items(): + flags += "{}\n".format(flag) + flags += indent(fill(fhelp, 80), '\t') + '\n\n' + flags += indent(fill("Long Form: "+str(cfg), 80), '\t') + '\n\n' + return flags + + def document_alias_help(self): + """Print the alias parts of the help.""" + + aliases = "The folowing aliases are defined:\n\n" + for alias, longname in NbConvertApp().aliases.items(): + aliases += "\t**{}** ({})\n\n".format(alias, longname) + return aliases + + def document_config_options(self): + """ + Provides a much improves version of the configuration documentation by + breaking the configuration options into app, exporter, writer, + preprocessor, postprocessor, and other sections. + """ + categories = {category: [c for c in self._classes_inc_parents() if category in c.__name__.lower()] + for category in ['app', 'exporter', 'writer', 'preprocessor', 'postprocessor']} + accounted_for = {c for category in categories.values() for c in category} + categories['other']= [c for c in self._classes_inc_parents() if c not in accounted_for] + + header = dedent(""" + {section} Options + ----------------------- + + """) + sections = "" + for category in categories: + sections += header.format(section=category.title()) + if category in ['exporter','preprocessor','writer']: + sections += ".. image:: _static/{image}_inheritance.png\n\n".format(image=category) + sections += '\n'.join(c.class_config_rst_doc() for c in categories[category]) + + return sections #----------------------------------------------------------------------------- # Main entry point From 2395bfd674803f278f07e483b32ebe75f7e37274 Mon Sep 17 00:00:00 2001 From: Tyler Makaro Date: Wed, 12 Jun 2019 19:41:19 -0700 Subject: [PATCH 337/671] update docstrings --- docs/source/conf.py | 2 +- nbconvert/nbconvertapp.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index 7693c28ce..6396ae490 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -20,7 +20,7 @@ # documentation root, use os.path.abspath to make it absolute, like shown here. #sys.path.insert(0, os.path.abspath('.')) -# Automatically generate the config_options.rst +# Automatically generate config_options.rst with open('../autogen_config.py') as f: exec(compile(f.read(), 'autogen_config.py', 'exec'), {}) diff --git a/nbconvert/nbconvertapp.py b/nbconvert/nbconvertapp.py index e0302cc66..e0bf77377 100755 --- a/nbconvert/nbconvertapp.py +++ b/nbconvert/nbconvertapp.py @@ -524,7 +524,7 @@ def document_flag_help(self): return flags def document_alias_help(self): - """Print the alias parts of the help.""" + """Return a string containing all of the aliases""" aliases = "The folowing aliases are defined:\n\n" for alias, longname in NbConvertApp().aliases.items(): From 44ba1e0e8545b8c42b51d030a8a660e5d2ab52c1 Mon Sep 17 00:00:00 2001 From: Tyler Makaro Date: Wed, 12 Jun 2019 19:43:27 -0700 Subject: [PATCH 338/671] replace NbConvertApp with self --- nbconvert/nbconvertapp.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/nbconvert/nbconvertapp.py b/nbconvert/nbconvertapp.py index e0bf77377..1bf5d4a25 100755 --- a/nbconvert/nbconvertapp.py +++ b/nbconvert/nbconvertapp.py @@ -517,7 +517,7 @@ def document_flag_help(self): Return a string containing descriptions of all the flags. """ flags = "The following flags are defined:\n\n" - for flag, (cfg, fhelp) in NbConvertApp().flags.items(): + for flag, (cfg, fhelp) in self.flags.items(): flags += "{}\n".format(flag) flags += indent(fill(fhelp, 80), '\t') + '\n\n' flags += indent(fill("Long Form: "+str(cfg), 80), '\t') + '\n\n' @@ -527,7 +527,7 @@ def document_alias_help(self): """Return a string containing all of the aliases""" aliases = "The folowing aliases are defined:\n\n" - for alias, longname in NbConvertApp().aliases.items(): + for alias, longname in self.aliases.items(): aliases += "\t**{}** ({})\n\n".format(alias, longname) return aliases @@ -541,7 +541,7 @@ def document_config_options(self): for category in ['app', 'exporter', 'writer', 'preprocessor', 'postprocessor']} accounted_for = {c for category in categories.values() for c in category} categories['other']= [c for c in self._classes_inc_parents() if c not in accounted_for] - + header = dedent(""" {section} Options ----------------------- From 35f5b37fe8ec20ccc2d889b13508958779a2f637 Mon Sep 17 00:00:00 2001 From: Tyler Makaro Date: Wed, 12 Jun 2019 20:06:36 -0700 Subject: [PATCH 339/671] fix python 2 --- nbconvert/nbconvertapp.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/nbconvert/nbconvertapp.py b/nbconvert/nbconvertapp.py index 1bf5d4a25..76a66648d 100755 --- a/nbconvert/nbconvertapp.py +++ b/nbconvert/nbconvertapp.py @@ -13,7 +13,8 @@ import sys import os import glob -from textwrap import fill, indent, dedent +from textwrap import fill, dedent +from ipython_genutils.text import indent from jupyter_core.application import JupyterApp, base_aliases, base_flags from traitlets.config import catch_config_error, Configurable @@ -519,8 +520,8 @@ def document_flag_help(self): flags = "The following flags are defined:\n\n" for flag, (cfg, fhelp) in self.flags.items(): flags += "{}\n".format(flag) - flags += indent(fill(fhelp, 80), '\t') + '\n\n' - flags += indent(fill("Long Form: "+str(cfg), 80), '\t') + '\n\n' + flags += indent(fill(fhelp, 80)) + '\n\n' + flags += indent(fill("Long Form: "+str(cfg), 80)) + '\n\n' return flags def document_alias_help(self): From 1caa58327d371fcd916f56dd34930a890be51c2f Mon Sep 17 00:00:00 2001 From: Tyler Makaro Date: Thu, 13 Jun 2019 17:23:52 -0700 Subject: [PATCH 340/671] Update docs/source/conf.py Co-Authored-By: Matthew Seal --- docs/source/conf.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index 6396ae490..620dac3b3 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -21,7 +21,8 @@ #sys.path.insert(0, os.path.abspath('.')) # Automatically generate config_options.rst -with open('../autogen_config.py') as f: +import os +with open(os.path.join(os.path.dirname(__file__), '..', 'autogen_config.py')) as f: exec(compile(f.read(), 'autogen_config.py', 'exec'), {}) # -- General configuration ------------------------------------------------ From 048fcf37f07640499b979acf51b9371eec0c3327 Mon Sep 17 00:00:00 2001 From: Tyler Makaro Date: Sat, 15 Jun 2019 10:11:40 -0700 Subject: [PATCH 341/671] fix_rts --- docs/source/conf.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index 620dac3b3..8fd59000d 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -14,14 +14,15 @@ # serve to show the default. import os +import sys # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. #sys.path.insert(0, os.path.abspath('.')) +sys.path.insert(0, os.path.abspath('../../.')) # path to nbconvert for autodoc # Automatically generate config_options.rst -import os with open(os.path.join(os.path.dirname(__file__), '..', 'autogen_config.py')) as f: exec(compile(f.read(), 'autogen_config.py', 'exec'), {}) From a1e99bf02001696603328d72f72c8590d6e50afa Mon Sep 17 00:00:00 2001 From: Tyler Makaro Date: Sat, 15 Jun 2019 21:32:19 -0700 Subject: [PATCH 342/671] fix delimiter in config docs --- nbconvert/nbconvertapp.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nbconvert/nbconvertapp.py b/nbconvert/nbconvertapp.py index 76a66648d..cb17ec774 100755 --- a/nbconvert/nbconvertapp.py +++ b/nbconvert/nbconvertapp.py @@ -555,7 +555,7 @@ def document_config_options(self): sections += ".. image:: _static/{image}_inheritance.png\n\n".format(image=category) sections += '\n'.join(c.class_config_rst_doc() for c in categories[category]) - return sections + return sections.replace(':',r'\:') #----------------------------------------------------------------------------- # Main entry point From 54ba0fcaf5f3bf41f99226cdb8c396f37ec1e572 Mon Sep 17 00:00:00 2001 From: Tyler Makaro Date: Sun, 16 Jun 2019 11:38:23 -0700 Subject: [PATCH 343/671] don't clobber all : --- nbconvert/nbconvertapp.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nbconvert/nbconvertapp.py b/nbconvert/nbconvertapp.py index cb17ec774..0745926e8 100755 --- a/nbconvert/nbconvertapp.py +++ b/nbconvert/nbconvertapp.py @@ -555,7 +555,7 @@ def document_config_options(self): sections += ".. image:: _static/{image}_inheritance.png\n\n".format(image=category) sections += '\n'.join(c.class_config_rst_doc() for c in categories[category]) - return sections.replace(':',r'\:') + return sections.replace(' : ',r' \: ') #----------------------------------------------------------------------------- # Main entry point From e152d3a2918c011ecf631097ea6887c2cdb6a878 Mon Sep 17 00:00:00 2001 From: Tyler Makaro Date: Tue, 18 Jun 2019 13:54:47 -0700 Subject: [PATCH 344/671] Return error if latex does --- nbconvert/exporters/pdf.py | 35 +++++++++++-------- .../exporters/tests/files/notebook2.ipynb | 2 +- 2 files changed, 21 insertions(+), 16 deletions(-) diff --git a/nbconvert/exporters/pdf.py b/nbconvert/exporters/pdf.py index 9f62d8b26..1efe162b9 100644 --- a/nbconvert/exporters/pdf.py +++ b/nbconvert/exporters/pdf.py @@ -73,9 +73,9 @@ class PDFExporter(LatexExporter): def _file_extension_default(self): return '.pdf' - def run_command(self, command_list, filename, count, log_function): + def run_command(self, command_list, filename, count, log_function, raise_on_failure=None): """Run command_list count times. - + Parameters ---------- command_list : list @@ -85,7 +85,10 @@ def run_command(self, command_list, filename, count, log_function): The name of the file to convert. count : int How many times to run the command. - + raise_on_failure: Exception class (default None) + If provided, will raise the given exception for if an instead of + returning False on command failure. + Returns ------- success : bool @@ -109,7 +112,7 @@ def run_command(self, command_list, filename, count, log_function): raise OSError("{formatter} not found on PATH, if you have not installed " "{formatter} you may need to do so. Find further instructions " "at {link}.".format(formatter=command_list[0], link=link)) - + times = 'time' if count == 1 else 'times' self.log.info("Running %s %i %s: %s", command_list[0], count, times, command) @@ -136,20 +139,24 @@ def run_command(self, command_list, filename, count, log_function): out = out.decode('utf-8', 'replace') log_function(command, out) self._captured_output.append(out) + if raise_on_failure: + raise raise_on_failure( + 'Failed to run "{command}" command:\n{output}'.format( + command=command, output=out)) return False # failure return True # success - def run_latex(self, filename): + def run_latex(self, filename, raise_on_failure=LatexFailed): """Run xelatex self.latex_count times.""" def log_error(command, out): self.log.critical(u"%s failed: %s\n%s", command[0], command, out) return self.run_command(self.latex_command, filename, - self.latex_count, log_error) + self.latex_count, log_error, raise_on_failure) - def run_bib(self, filename): - """Run bibtex self.latex_count times.""" + def run_bib(self, filename, raise_on_failure=False): + """Run bibtex one time.""" filename = os.path.splitext(filename)[0] def log_error(command, out): @@ -157,7 +164,7 @@ def log_error(command, out): command[0]) self.log.debug(u"%s output: %s\n%s", command[0], command, out) - return self.run_command(self.bib_command, filename, 1, log_error) + return self.run_command(self.bib_command, filename, 1, log_error, raise_on_failure) def from_notebook_node(self, nb, resources=None, **kw): latex, resources = super(PDFExporter, self).from_notebook_node( @@ -175,12 +182,10 @@ def from_notebook_node(self, nb, resources=None, **kw): resources['output_extension'] = '.tex' tex_file = self.writer.write(latex, resources, notebook_name=notebook_name) self.log.info("Building PDF") - rc = self.run_latex(tex_file) - if rc: - rc = self.run_bib(tex_file) - if rc: - rc = self.run_latex(tex_file) - + self.run_latex(tex_file) + if self.run_bib(tex_file): + self.run_latex(tex_file) + pdf_file = notebook_name + '.pdf' if not os.path.isfile(pdf_file): raise LatexFailed('\n'.join(self._captured_output)) diff --git a/nbconvert/exporters/tests/files/notebook2.ipynb b/nbconvert/exporters/tests/files/notebook2.ipynb index b1f0a553d..1ff2e7532 100644 --- a/nbconvert/exporters/tests/files/notebook2.ipynb +++ b/nbconvert/exporters/tests/files/notebook2.ipynb @@ -178,7 +178,7 @@ "metadata": {}, "source": [ "Make sure markdown parser doesn't crash with empty Latex formulas blocks\n", - "$$$$\n", + "$$ $$\n", "\\[\\]\n", "$$" ] From c988bf5bb51801fb24fdf1473428cba43c19f99c Mon Sep 17 00:00:00 2001 From: Tyler Makaro Date: Tue, 18 Jun 2019 15:13:16 -0700 Subject: [PATCH 345/671] add underscores to extra files dir --- nbconvert/nbconvertapp.py | 2 +- nbconvert/preprocessors/extractoutput.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/nbconvert/nbconvertapp.py b/nbconvert/nbconvertapp.py index 0745926e8..9da036bd2 100755 --- a/nbconvert/nbconvertapp.py +++ b/nbconvert/nbconvertapp.py @@ -172,7 +172,7 @@ def _classes_default(self): output_files_dir = Unicode('{notebook_name}_files', help='''Directory to copy extra files (figures) to. '{notebook_name}' in the string will be converted to notebook - basename''' + basename. Spaces will be replaced with underscores.''' ).tag(config=True) examples = Unicode(u""" diff --git a/nbconvert/preprocessors/extractoutput.py b/nbconvert/preprocessors/extractoutput.py index eb8c8594e..250c7915e 100755 --- a/nbconvert/preprocessors/extractoutput.py +++ b/nbconvert/preprocessors/extractoutput.py @@ -72,8 +72,8 @@ def preprocess_cell(self, cell, resources, cell_index): #Get the unique key from the resource dict if it exists. If it does not #exist, use 'output' as the default. Also, get files directory if it #has been specified - unique_key = resources.get('unique_key', 'output') - output_files_dir = resources.get('output_files_dir', None) + unique_key = resources.get('unique_key', 'output').replace(' ', '_') + output_files_dir = resources.get('output_files_dir', None).replace(' ', '_') #Make sure outputs key exists if not isinstance(resources['outputs'], dict): From bc2ae890588871d5bd55376eb26f59265e449131 Mon Sep 17 00:00:00 2001 From: Tyler Makaro Date: Tue, 18 Jun 2019 15:55:06 -0700 Subject: [PATCH 346/671] replace spaces in the extracted files with underscores --- nbconvert/exporters/latex.py | 3 ++- nbconvert/nbconvertapp.py | 3 ++- nbconvert/preprocessors/extractoutput.py | 15 ++++++++++++--- 3 files changed, 16 insertions(+), 5 deletions(-) diff --git a/nbconvert/exporters/latex.py b/nbconvert/exporters/latex.py index eae351308..ed5ddc78f 100644 --- a/nbconvert/exporters/latex.py +++ b/nbconvert/exporters/latex.py @@ -56,7 +56,8 @@ def default_config(self): 'display_data_priority' : ['text/latex', 'application/pdf', 'image/png', 'image/jpeg', 'image/svg+xml', 'text/markdown', 'text/plain'] }, 'ExtractOutputPreprocessor': { - 'enabled':True + 'enabled':True, + 'escape_dir_spaces':True }, 'SVG2PDFPreprocessor': { 'enabled':True diff --git a/nbconvert/nbconvertapp.py b/nbconvert/nbconvertapp.py index 9da036bd2..9809db704 100755 --- a/nbconvert/nbconvertapp.py +++ b/nbconvert/nbconvertapp.py @@ -172,7 +172,8 @@ def _classes_default(self): output_files_dir = Unicode('{notebook_name}_files', help='''Directory to copy extra files (figures) to. '{notebook_name}' in the string will be converted to notebook - basename. Spaces will be replaced with underscores.''' + basename. Spaces will be replaced with underscores when using + the latex/pdf exporters.''' ).tag(config=True) examples = Unicode(u""" diff --git a/nbconvert/preprocessors/extractoutput.py b/nbconvert/preprocessors/extractoutput.py index 250c7915e..effc37010 100755 --- a/nbconvert/preprocessors/extractoutput.py +++ b/nbconvert/preprocessors/extractoutput.py @@ -12,7 +12,7 @@ import json from mimetypes import guess_extension -from traitlets import Unicode, Set +from traitlets import Unicode, Set, Bool from .base import Preprocessor if sys.version_info < (3,): @@ -54,6 +54,11 @@ class ExtractOutputPreprocessor(Preprocessor): {'image/png', 'image/jpeg', 'image/svg+xml', 'application/pdf'} ).tag(config=True) + escape_dir_spaces = Bool( + False, help="If True, spaces will be replaced with underscores in the " + "output directory." + ).tag(config=True) + def preprocess_cell(self, cell, resources, cell_index): """ Apply a transformation on each cell, @@ -72,8 +77,12 @@ def preprocess_cell(self, cell, resources, cell_index): #Get the unique key from the resource dict if it exists. If it does not #exist, use 'output' as the default. Also, get files directory if it #has been specified - unique_key = resources.get('unique_key', 'output').replace(' ', '_') - output_files_dir = resources.get('output_files_dir', None).replace(' ', '_') + unique_key = resources.get('unique_key', 'output') + output_files_dir = resources.get('output_files_dir', None) + if self.escape_dir_spaces: + unique_key = unique_key.replace(' ', '_') + if output_files_dir is not None: + output_files_dir = unique_key.replace(' ', '_') #Make sure outputs key exists if not isinstance(resources['outputs'], dict): From 718fa248e2c902b7e57a1a04ddc417d776047a3e Mon Sep 17 00:00:00 2001 From: Tyler Makaro Date: Tue, 18 Jun 2019 16:38:41 -0700 Subject: [PATCH 347/671] ensure that output_files_dir is returned from resources properly --- nbconvert/exporters/pdf.py | 2 +- nbconvert/preprocessors/extractoutput.py | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/nbconvert/exporters/pdf.py b/nbconvert/exporters/pdf.py index 1efe162b9..a84824362 100644 --- a/nbconvert/exporters/pdf.py +++ b/nbconvert/exporters/pdf.py @@ -175,7 +175,7 @@ def from_notebook_node(self, nb, resources=None, **kw): self.texinputs = resources['metadata']['path'] else: self.texinputs = getcwd() - + self._captured_outputs = [] with TemporaryWorkingDirectory(): notebook_name = 'notebook' diff --git a/nbconvert/preprocessors/extractoutput.py b/nbconvert/preprocessors/extractoutput.py index effc37010..1066658e4 100755 --- a/nbconvert/preprocessors/extractoutput.py +++ b/nbconvert/preprocessors/extractoutput.py @@ -81,8 +81,11 @@ def preprocess_cell(self, cell, resources, cell_index): output_files_dir = resources.get('output_files_dir', None) if self.escape_dir_spaces: unique_key = unique_key.replace(' ', '_') + resources['unique_key'] = unique_key if output_files_dir is not None: - output_files_dir = unique_key.replace(' ', '_') + output_files_dir = output_files_dir.replace(' ', '_') + resources['output_files_dir'] = output_files_dir + #Make sure outputs key exists if not isinstance(resources['outputs'], dict): From 194c9756a01e74aab0ad067f81bb6cab9a54ac66 Mon Sep 17 00:00:00 2001 From: Kunal Marwaha Date: Sat, 22 Jun 2019 11:19:54 -0700 Subject: [PATCH 348/671] typo: developping -> developing --- docs/source/nbconvert_library.ipynb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/nbconvert_library.ipynb b/docs/source/nbconvert_library.ipynb index 2928c5841..b31749340 100644 --- a/docs/source/nbconvert_library.ipynb +++ b/docs/source/nbconvert_library.ipynb @@ -657,7 +657,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "@damianavila wrote the Nikola Plugin to [write blog post as Notebooks](http://www.damian.oquanta.info/posts/one-line-deployment-of-your-site-to-gh-pages.html) and is developping a js-extension to publish notebooks via one click from the web app." + "@damianavila wrote the Nikola Plugin to [write blog post as Notebooks](http://www.damian.oquanta.info/posts/one-line-deployment-of-your-site-to-gh-pages.html) and is developing a js-extension to publish notebooks via one click from the web app." ] }, { From 40989d859da65bb3e3a3c588e5eeb2ff6cfc9cd1 Mon Sep 17 00:00:00 2001 From: Wayne Witzel Date: Wed, 26 Jun 2019 08:12:39 -0600 Subject: [PATCH 349/671] Added 'store_history' option to 'preprocess_cell' and 'run_cell' methods of ExecutePreprocessor. --- nbconvert/preprocessors/execute.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/nbconvert/preprocessors/execute.py b/nbconvert/preprocessors/execute.py index 0272290b8..e8246a78d 100644 --- a/nbconvert/preprocessors/execute.py +++ b/nbconvert/preprocessors/execute.py @@ -426,7 +426,7 @@ def set_widgets_metadata(self): if buffers: widget['buffers'] = buffers - def preprocess_cell(self, cell, resources, cell_index): + def preprocess_cell(self, cell, resources, cell_index, store_history=True): """ Executes a single code cell. See base.py for details. @@ -435,7 +435,7 @@ def preprocess_cell(self, cell, resources, cell_index): if cell.cell_type != 'code' or not cell.source.strip(): return cell, resources - reply, outputs = self.run_cell(cell, cell_index) + reply, outputs = self.run_cell(cell, cell_index, store_history) # Backwards compatibility for processes that wrap run_cell cell.outputs = outputs @@ -542,9 +542,9 @@ def _passed_deadline(self, deadline): return True return False - def run_cell(self, cell, cell_index=0): - parent_msg_id = self.kc.execute( - cell.source, stop_on_error=not self.allow_errors) + def run_cell(self, cell, cell_index=0, store_history=True): + parent_msg_id = self.kc.execute(cell.source, + store_history=store_history, stop_on_error=not self.allow_errors) self.log.debug("Executing cell:\n%s", cell.source) exec_timeout = self._get_timeout(cell) deadline = None From 53e22d1386442e4d73d33875321a72a03d720e83 Mon Sep 17 00:00:00 2001 From: Alexander Kapshuna Date: Wed, 3 Jul 2019 19:55:28 +0300 Subject: [PATCH 350/671] Require mock only on Python 2 Only usage of mock is inside "except ImportError" meant for running tests under Python 2. So there is no need to install it for any Python 3. --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index deca85375..dd7c62864 100644 --- a/setup.py +++ b/setup.py @@ -212,7 +212,7 @@ def run(self): jupyter_client_req = 'jupyter_client>=4.2' extra_requirements = { - 'test': ['pytest', 'pytest-cov', 'mock', 'ipykernel', jupyter_client_req, 'ipywidgets>=7'], + 'test': ['pytest', 'pytest-cov', 'mock; python_version < "3.4"', 'ipykernel', jupyter_client_req, 'ipywidgets>=7'], 'serve': ['tornado>=4.0'], 'execute': [jupyter_client_req], 'docs': ['sphinx>=1.5.1', From 76061f8df164a742fc039b2574508bd8cf7d96a1 Mon Sep 17 00:00:00 2001 From: Alexander Rudy Date: Mon, 8 Jul 2019 18:54:48 -0400 Subject: [PATCH 351/671] Tests which fail in multiprocessing contexts (#1018) Adds Parallel Execution Tests --- .gitignore | 4 + ...Execute.ipynb => Parallel Execute A.ipynb} | 25 +++++- .../tests/files/Parallel Execute B.ipynb | 84 +++++++++++++++++++ .../preprocessors/tests/files/Sleep One.ipynb | 50 +++++++++++ nbconvert/preprocessors/tests/test_execute.py | 56 +++++++++---- setup.py | 4 +- 6 files changed, 200 insertions(+), 23 deletions(-) rename nbconvert/preprocessors/tests/files/{Parallel Execute.ipynb => Parallel Execute A.ipynb} (75%) create mode 100644 nbconvert/preprocessors/tests/files/Parallel Execute B.ipynb create mode 100644 nbconvert/preprocessors/tests/files/Sleep One.ipynb diff --git a/.gitignore b/.gitignore index 7dd734fcf..86083f990 100644 --- a/.gitignore +++ b/.gitignore @@ -23,7 +23,11 @@ htmlcov .cache docs/source/*.tpl docs/source/config_options.rst + # Eclipse pollutes the filesystem .project .pydevproject .settings + +# VSCode +.vscode \ No newline at end of file diff --git a/nbconvert/preprocessors/tests/files/Parallel Execute.ipynb b/nbconvert/preprocessors/tests/files/Parallel Execute A.ipynb similarity index 75% rename from nbconvert/preprocessors/tests/files/Parallel Execute.ipynb rename to nbconvert/preprocessors/tests/files/Parallel Execute A.ipynb index d40545c30..699abbcca 100644 --- a/nbconvert/preprocessors/tests/files/Parallel Execute.ipynb +++ b/nbconvert/preprocessors/tests/files/Parallel Execute A.ipynb @@ -9,7 +9,7 @@ "This notebook uses a file system based \"lock\" to assert that two instances of the notebook kernel will run in parallel. Each instance writes to a file in a temporary directory, and then tries to read the other file from\n", "the temporary directory, so that running them in sequence will fail, but running them in parallel will succed.\n", "\n", - "Two notebooks are launched, each with an injected cell which sets the `this_notebook` variable. One notebook is set to `this_notebook = 'A'` and the other `this_notebook = 'B'`." + "Two notebooks are launched, each which sets the `this_notebook` variable. One notebook is set to `this_notebook = 'A'` and the other `this_notebook = 'B'`." ] }, { @@ -31,7 +31,8 @@ "outputs": [], "source": [ "# the variable this_notebook is injectected in a cell above by the test framework.\n", - "other_notebook = {'A':'B', 'B':'A'}[this_notebook]\n", + "this_notebook = 'A'\n", + "other_notebook = 'B'\n", "directory = os.environ['NBEXECUTE_TEST_PARALLEL_TMPDIR']\n", "with open(os.path.join(directory, 'test_file_{}.txt'.format(this_notebook)), 'w') as f:\n", " f.write('Hello from {}'.format(this_notebook))" @@ -59,7 +60,25 @@ ] } ], - "metadata": {}, + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.6.7" + } + }, "nbformat": 4, "nbformat_minor": 2 } diff --git a/nbconvert/preprocessors/tests/files/Parallel Execute B.ipynb b/nbconvert/preprocessors/tests/files/Parallel Execute B.ipynb new file mode 100644 index 000000000..54bd6ab91 --- /dev/null +++ b/nbconvert/preprocessors/tests/files/Parallel Execute B.ipynb @@ -0,0 +1,84 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Ensure notebooks can execute in parallel\n", + "\n", + "This notebook uses a file system based \"lock\" to assert that two instances of the notebook kernel will run in parallel. Each instance writes to a file in a temporary directory, and then tries to read the other file from\n", + "the temporary directory, so that running them in sequence will fail, but running them in parallel will succed.\n", + "\n", + "Two notebooks are launched, each which sets the `this_notebook` variable. One notebook is set to `this_notebook = 'A'` and the other `this_notebook = 'B'`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "import os.path\n", + "import tempfile\n", + "import time" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# the variable this_notebook is injectected in a cell above by the test framework.\n", + "this_notebook = 'B'\n", + "other_notebook = 'A'\n", + "directory = os.environ['NBEXECUTE_TEST_PARALLEL_TMPDIR']\n", + "with open(os.path.join(directory, 'test_file_{}.txt'.format(this_notebook)), 'w') as f:\n", + " f.write('Hello from {}'.format(this_notebook))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "start = time.time()\n", + "timeout = 5\n", + "end = start + timeout\n", + "target_file = os.path.join(directory, 'test_file_{}.txt'.format(other_notebook))\n", + "while time.time() < end:\n", + " time.sleep(0.1)\n", + " if os.path.exists(target_file):\n", + " with open(target_file, 'r') as f:\n", + " text = f.read()\n", + " if text == 'Hello from {}'.format(other_notebook):\n", + " break\n", + "else:\n", + " assert False, \"Timed out – didn't get a message from {}\".format(other_notebook)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.6.7" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/nbconvert/preprocessors/tests/files/Sleep One.ipynb b/nbconvert/preprocessors/tests/files/Sleep One.ipynb new file mode 100644 index 000000000..d161b6e13 --- /dev/null +++ b/nbconvert/preprocessors/tests/files/Sleep One.ipynb @@ -0,0 +1,50 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import time" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "time.sleep(0.01)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.7.0" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/nbconvert/preprocessors/tests/test_execute.py b/nbconvert/preprocessors/tests/test_execute.py index 1ca6ac3ed..ce7a95f46 100644 --- a/nbconvert/preprocessors/tests/test_execute.py +++ b/nbconvert/preprocessors/tests/test_execute.py @@ -32,6 +32,7 @@ from nbconvert.filters import strip_ansi from testpath import modified_env from ipython_genutils.py3compat import string_types +from pebble import ProcessPool try: from queue import Empty # Py 3 @@ -46,6 +47,11 @@ except ImportError: from mock import MagicMock, patch # Py 2 + +PY3 = False +if sys.version_info[0] >= 3: + PY3 = True + addr_pat = re.compile(r'0x[0-9a-f]{7,9}') ipython_input_pat = re.compile(r'') current_dir = os.path.dirname(__file__) @@ -74,7 +80,7 @@ def build_preprocessor(opts): return preprocessor -def run_notebook(filename, opts, resources, preprocess_notebook=None): +def run_notebook(filename, opts, resources): """Loads and runs a notebook, returning both the version prior to running it and the version after running it. @@ -82,9 +88,6 @@ def run_notebook(filename, opts, resources, preprocess_notebook=None): with io.open(filename) as f: input_nb = nbformat.read(f, 4) - if preprocess_notebook: - input_nb = preprocess_notebook(input_nb) - preprocessor = build_preprocessor(opts) cleaned_input_nb = copy.deepcopy(input_nb) for cell in cleaned_input_nb.cells: @@ -264,24 +267,14 @@ def test_run_all_notebooks(input_name, opts): assert_notebooks_equal(input_nb, output_nb) -def label_parallel_notebook(nb, label): - """Insert a cell in a notebook which sets the variable `this_notebook` to the string `label`. - - Used for parallel testing to label two notebooks which are run simultaneously. - """ - label_cell = nbformat.v4.new_code_cell(source="this_notebook = '{}'".format(label)) - nb.cells.insert(1, label_cell) - return nb - - def test_parallel_notebooks(capfd, tmpdir): """Two notebooks should be able to be run simultaneously without problems. - + The two notebooks spawned here use the filesystem to check that the other notebook wrote to the filesystem.""" opts = dict(kernel_name="python") - input_name = "Parallel Execute.ipynb" + input_name = "Parallel Execute {label}.ipynb" input_file = os.path.join(current_dir, "files", input_name) res = notebook_resources() @@ -290,10 +283,9 @@ def test_parallel_notebooks(capfd, tmpdir): threading.Thread( target=run_notebook, args=( - input_file, + input_file.format(label=label), opts, res, - functools.partial(label_parallel_notebook, label=label), ), ) for label in ("A", "B") @@ -304,6 +296,34 @@ def test_parallel_notebooks(capfd, tmpdir): captured = capfd.readouterr() assert captured.err == "" +@pytest.mark.skipif(not PY3, + reason = "Not tested for Python 2") +def test_many_parallel_notebooks(capfd): + """Ensure that when many IPython kernels are run in parallel, nothing awful happens. + + Specifically, many IPython kernels when run simultaneously would enocunter errors + due to using the same SQLite history database. + """ + opts = dict(kernel_name="python", timeout=5) + input_name = "HelloWorld.ipynb" + input_file = os.path.join(current_dir, "files", input_name) + res = PreprocessorTestsBase().build_resources() + res["metadata"]["path"] = os.path.join(current_dir, "files") + + # run once, to trigger creating the original context + run_notebook(input_file, opts, res) + + with ProcessPool(max_workers=4) as pool: + futures = [ + # Travis needs a lot more time even though 10s is enough on most dev machines + pool.schedule(run_notebook, args=(input_file, opts, res), timeout=30) + for i in range(0, 8) + ] + for index, future in enumerate(futures): + future.result() + + captured = capfd.readouterr() + assert captured.err == "" class TestExecute(PreprocessorTestsBase): """Contains test functions for execute.py""" diff --git a/setup.py b/setup.py index dd7c62864..580838320 100644 --- a/setup.py +++ b/setup.py @@ -209,10 +209,10 @@ def run(self): 'testpath', 'defusedxml', ] -jupyter_client_req = 'jupyter_client>=4.2' +jupyter_client_req = 'jupyter_client>=4.3' extra_requirements = { - 'test': ['pytest', 'pytest-cov', 'mock; python_version < "3.4"', 'ipykernel', jupyter_client_req, 'ipywidgets>=7'], + 'test': ['pytest', 'pytest-cov', 'mock; python_version < "3.4"', 'ipykernel', jupyter_client_req, 'ipywidgets>=7', 'pebble'], 'serve': ['tornado>=4.0'], 'execute': [jupyter_client_req], 'docs': ['sphinx>=1.5.1', From 526a40506bec61f21bf2957df44e0e734c48d358 Mon Sep 17 00:00:00 2001 From: KrokodileDandy <40100920+KrokodileDandy@users.noreply.github.com> Date: Fri, 19 Jul 2019 14:49:45 +0200 Subject: [PATCH 352/671] Update base.tplx \usepackage[T1]{fontenc} shouldn't be used with xelatex and throws errors like "!Text line contains an invalid character". \usepackage{fontspec} recommended. --- nbconvert/templates/latex/base.tplx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nbconvert/templates/latex/base.tplx b/nbconvert/templates/latex/base.tplx index 74cd65e84..6508067a7 100644 --- a/nbconvert/templates/latex/base.tplx +++ b/nbconvert/templates/latex/base.tplx @@ -13,7 +13,7 @@ This template does not define a docclass, the inheriting class must define this. ((* block docclass *))((* endblock docclass *)) ((* block packages *)) - \usepackage[T1]{fontenc} + \usepackage{fontspec} % Nicer default font (+ math font) than Computer Modern for most use cases \usepackage{mathpazo} From 05c2dc5248e1b66896f55af064e908bd61ba178a Mon Sep 17 00:00:00 2001 From: Nils Japke Date: Wed, 24 Jul 2019 16:11:54 +0200 Subject: [PATCH 353/671] Fix for ANSI characters in output of code block in style_jupyter.tplx --- nbconvert/templates/latex/style_jupyter.tplx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nbconvert/templates/latex/style_jupyter.tplx b/nbconvert/templates/latex/style_jupyter.tplx index dc3843e67..3d7a08580 100644 --- a/nbconvert/templates/latex/style_jupyter.tplx +++ b/nbconvert/templates/latex/style_jupyter.tplx @@ -129,7 +129,7 @@ ((* block execute_result scoped *)) ((*- for type in output.data | filter_data_type -*)) ((*- if type in ['text/plain']*)) - ((( draw_cell(output.data['text/plain'] | wrap_text(charlim) | escape_latex, cell, 'Out', 'outcolor', '\\boxspacing') ))) + ((( draw_cell(output.data['text/plain'] | wrap_text(charlim) | escape_latex | ansi2latex, cell, 'Out', 'outcolor', '\\boxspacing') ))) ((* else -*)) ((( " " ))) ((( draw_prompt(cell, 'Out', 'outcolor','') )))((( super() ))) From 9277138bf33f7dd0308e0665bd1cd4893bb3ec63 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20F=C3=BChr?= <40100920+KrokodileDandy@users.noreply.github.com> Date: Fri, 26 Jul 2019 13:33:55 +0200 Subject: [PATCH 354/671] Updated base.tplx Removed \usepackage[utf8]{inputenc} --- nbconvert/templates/latex/base.tplx | 1 - 1 file changed, 1 deletion(-) diff --git a/nbconvert/templates/latex/base.tplx b/nbconvert/templates/latex/base.tplx index 6508067a7..358eb7977 100644 --- a/nbconvert/templates/latex/base.tplx +++ b/nbconvert/templates/latex/base.tplx @@ -46,7 +46,6 @@ This template does not define a docclass, the inheriting class must define this. \usepackage{upquote} % Upright quotes for verbatim code \usepackage{eurosym} % defines \euro \usepackage[mathletters]{ucs} % Extended unicode (utf-8) support - \usepackage[utf8x]{inputenc} % Allow utf-8 characters in the tex document \usepackage{fancyvrb} % verbatim replacement that allows latex \usepackage{grffile} % extends the file name processing of package graphics % to support a larger range From 2c98db9aad1877e5fd746b626a88c3434b3c331f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20F=C3=BChr?= <40100920+KrokodileDandy@users.noreply.github.com> Date: Fri, 26 Jul 2019 15:38:46 +0200 Subject: [PATCH 355/671] Update .travis.yml Added the latex package lmodern --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index f0f1c99d0..ccc955f66 100644 --- a/.travis.yml +++ b/.travis.yml @@ -27,6 +27,7 @@ addons: - cm-super # more fonts - texlive-xetex # latex to pdf converter - inkscape # for svgs in pdf output + - lmodern # latex package install: - wget https://github.com/jgm/pandoc/releases/download/2.7/pandoc-2.7-1-amd64.deb && sudo dpkg -i pandoc-2.7-1-amd64.deb - pip install --upgrade setuptools pip pytest From 3036166bcc35ab82f629ca9c35ca087551d5b4b6 Mon Sep 17 00:00:00 2001 From: Matthias Geier Date: Sat, 27 Jul 2019 19:36:39 +0200 Subject: [PATCH 356/671] Pip-install nbconvert on readthedocs.org --- .readthedocs.yml | 3 +++ docs/source/conf.py | 2 -- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.readthedocs.yml b/.readthedocs.yml index cdd350487..0a842d7bd 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -18,3 +18,6 @@ build: python: version: 3.7 + install: + - method: pip + path: . diff --git a/docs/source/conf.py b/docs/source/conf.py index 8fd59000d..29d7eb5c7 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -14,13 +14,11 @@ # serve to show the default. import os -import sys # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. #sys.path.insert(0, os.path.abspath('.')) -sys.path.insert(0, os.path.abspath('../../.')) # path to nbconvert for autodoc # Automatically generate config_options.rst with open(os.path.join(os.path.dirname(__file__), '..', 'autogen_config.py')) as f: From 48a4698155a69ba0fadeb93d105a01cbbed468e4 Mon Sep 17 00:00:00 2001 From: Tyler Makaro Date: Sun, 28 Jul 2019 10:48:21 -0700 Subject: [PATCH 357/671] make a default global location for custom user templates (#1028) Make a default global locations for custom user templates --- docs/source/customizing.ipynb | 44 +++++++++++++++++++++++-- nbconvert/exporters/html.py | 5 +++ nbconvert/exporters/latex.py | 5 +++ nbconvert/exporters/templateexporter.py | 15 +++++++++ 4 files changed, 67 insertions(+), 2 deletions(-) diff --git a/docs/source/customizing.ipynb b/docs/source/customizing.ipynb index 4da09ebd1..ec780c74a 100644 --- a/docs/source/customizing.ipynb +++ b/docs/source/customizing.ipynb @@ -206,6 +206,40 @@ "!jupyter nbconvert --to python 'example.ipynb' --stdout --template=simplepython.tpl" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Saving Custom Templates\n", + "\n", + "By default, nbconvert finds templates from a few locations.\n", + "\n", + "The recommended place to save custom templates, so that they are globally accessible to nbconvert, is your jupyter data directories:\n", + "\n", + "- share/jupyter\n", + " - nbconvert\n", + " - templates\n", + " - html\n", + " - latex\n", + "\n", + "The HTML and LaTeX/PDF exporters will search the html and latex subdirectories for templates, respectively.\n", + "\n", + "To find your jupyter configuration directory you can use:\n", + "\n", + "```python\n", + "from jupyter_core.paths import jupyter_path\n", + "print(jupyter_path('nbconvert','templates'))\n", + "```\n", + "\n", + "Additionally,\n", + "\n", + "```python\n", + "TemplateExporter.template_path=['.']\n", + "```\n", + "\n", + "defines an additional list of paths that nbconvert can look for user defined templates. It defaults to searching for custom templates in the current working directory and can be changed through configuration options." + ] + }, { "cell_type": "markdown", "metadata": {}, @@ -376,7 +410,13 @@ "source": [ "### A few gotchas\n", "\n", - "Jinja blocks use `{% %}` by default which does not play nicely with LaTeX, so those are replaced by `((* *))` in LaTeX templates." + "Jinja uses `%`, `{`, and `}` for syntax by default which does not play nicely with LaTeX. In LaTeX, we have the following replacements:\n", + "\n", + "| Syntax | Default | LaTeX |\n", + "|----------|---------|---------|\n", + "| block | {% %} | ((* *)) |\n", + "| variable | {{ }} | ((( ))) |\n", + "| comment | {# #} | ((= =)) |" ] }, { @@ -24921,7 +24961,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.6.1" + "version": "3.7.3" } }, "nbformat": 4, diff --git a/nbconvert/exporters/html.py b/nbconvert/exporters/html.py index cf6d0871b..bf2b3c005 100644 --- a/nbconvert/exporters/html.py +++ b/nbconvert/exporters/html.py @@ -8,6 +8,7 @@ from traitlets import default, Unicode from traitlets.config import Config +from jupyter_core.paths import jupyter_path from jinja2 import contextfilter from nbconvert.filters.highlight import Highlight2HTML @@ -36,6 +37,10 @@ def _file_extension_default(self): def _default_template_path_default(self): return os.path.join("..", "templates", "html") + @default('template_data_paths') + def _template_data_paths_default(self): + return jupyter_path("nbconvert", "templates", "html") + @default('template_file') def _template_file_default(self): return 'full.tpl' diff --git a/nbconvert/exporters/latex.py b/nbconvert/exporters/latex.py index eae351308..b9f82eb4e 100644 --- a/nbconvert/exporters/latex.py +++ b/nbconvert/exporters/latex.py @@ -7,6 +7,7 @@ from traitlets import Unicode, default from traitlets.config import Config +from jupyter_core.paths import jupyter_path from nbconvert.filters.highlight import Highlight2Latex from nbconvert.filters.filter_links import resolve_references @@ -38,6 +39,10 @@ def _default_template_path_default(self): @default('template_skeleton_path') def _template_skeleton_path_default(self): return os.path.join("..", "templates", "latex", "skeleton") + + @default('template_data_paths') + def _template_data_paths_default(self): + return jupyter_path("nbconvert", "templates", "latex") #Extension that the template files use. template_extension = Unicode(".tplx").tag(config=True) diff --git a/nbconvert/exporters/templateexporter.py b/nbconvert/exporters/templateexporter.py index 77059428c..4a3fff78b 100644 --- a/nbconvert/exporters/templateexporter.py +++ b/nbconvert/exporters/templateexporter.py @@ -15,6 +15,8 @@ from traitlets.config import Config from traitlets.utils.importstring import import_item from ipython_genutils import py3compat +from jupyter_core.paths import jupyter_path +from jupyter_core.utils import ensure_dir_exists from jinja2 import ( TemplateNotFound, Environment, ChoiceLoader, FileSystemLoader, BaseLoader, DictLoader @@ -184,6 +186,11 @@ def _raw_template_changed(self, change): help="Path where the template skeleton files are located.", ).tag(affects_environment=True) + template_data_paths = List( + jupyter_path('nbconvert','templates'), + help="Path where templates can be installed too." + ).tag(affects_environment=True) + #Extension that the template files use. template_extension = Unicode(".tpl").tag(config=True, affects_environment=True) @@ -391,7 +398,15 @@ def _create_environment(self): """ here = os.path.dirname(os.path.realpath(__file__)) + additional_paths = self.template_data_paths + for path in additional_paths: + try: + ensure_dir_exists(path, mode=0o700) + except OSError: + pass + paths = self.template_path + \ + additional_paths + \ [os.path.join(here, self.default_template_path), os.path.join(here, self.template_skeleton_path)] From 3b19415802a1d0fd115916a55294df278c793207 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20F=C3=BChr?= <40100920+KrokodileDandy@users.noreply.github.com> Date: Mon, 29 Jul 2019 12:10:25 +0200 Subject: [PATCH 358/671] Removed package mathpazo --- nbconvert/templates/latex/base.tplx | 2 -- 1 file changed, 2 deletions(-) diff --git a/nbconvert/templates/latex/base.tplx b/nbconvert/templates/latex/base.tplx index 358eb7977..64f13d6da 100644 --- a/nbconvert/templates/latex/base.tplx +++ b/nbconvert/templates/latex/base.tplx @@ -14,8 +14,6 @@ This template does not define a docclass, the inheriting class must define this. ((* block packages *)) \usepackage{fontspec} - % Nicer default font (+ math font) than Computer Modern for most use cases - \usepackage{mathpazo} % Basic figure setup, for now with no caption control since it's done % automatically by Pandoc (which extracts ![](path) syntax from Markdown). From 365683f40f85dc9304b6f1c23ca35eeb4d63b597 Mon Sep 17 00:00:00 2001 From: Hsiaoming Yang Date: Tue, 30 Jul 2019 11:55:49 +0900 Subject: [PATCH 359/671] Lock mistune version Mistune will release a 2.0 version in the future, which will break the API. I'll submit a patch for 2.0 when it is released. --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 580838320..38e10861e 100644 --- a/setup.py +++ b/setup.py @@ -197,7 +197,7 @@ def run(self): ) setup_args['install_requires'] = [ - 'mistune>=0.8.1', + 'mistune>=0.8.1,<2', 'jinja2>=2.4', 'pygments', 'traitlets>=4.2', From 86c9dad207d38b9382c81bfb285ac1e9684d8e30 Mon Sep 17 00:00:00 2001 From: Tyler Makaro Date: Tue, 30 Jul 2019 10:58:14 -0700 Subject: [PATCH 360/671] fix #785 --- nbconvert/exporters/exporter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nbconvert/exporters/exporter.py b/nbconvert/exporters/exporter.py index 56eccd4f4..ffd6af4de 100644 --- a/nbconvert/exporters/exporter.py +++ b/nbconvert/exporters/exporter.py @@ -168,7 +168,7 @@ def from_filename(self, filename, resources=None, **kw): if not 'metadata' in resources or resources['metadata'] == '': resources['metadata'] = ResourcesDict() path, basename = os.path.split(filename) - notebook_name = basename[:basename.rfind('.')] + notebook_name = os.path.splitext(basename)[0] resources['metadata']['name'] = notebook_name resources['metadata']['path'] = path From f1ec3553dffdebe21ea886a0f4f50677d71726a0 Mon Sep 17 00:00:00 2001 From: Tyler Makaro Date: Tue, 30 Jul 2019 13:52:54 -0700 Subject: [PATCH 361/671] Make style_*.tplx files useable on there own, and fixes some whitespace. --- nbconvert/templates/latex/article.tplx | 6 +++--- nbconvert/templates/latex/base.tplx | 11 ++++++----- nbconvert/templates/latex/skeleton/null.tplx | 4 ++-- nbconvert/templates/skeleton/Makefile | 4 ++-- 4 files changed, 13 insertions(+), 12 deletions(-) diff --git a/nbconvert/templates/latex/article.tplx b/nbconvert/templates/latex/article.tplx index 8588c850c..eb26195b4 100644 --- a/nbconvert/templates/latex/article.tplx +++ b/nbconvert/templates/latex/article.tplx @@ -1,8 +1,8 @@ ((=- Default to the notebook output style -=)) -((* if not cell_style is defined *)) +((*- if not cell_style is defined -*)) ((* set cell_style = 'style_jupyter.tplx' *)) -((* endif *)) +((*- endif -*)) ((=- Inherit from the specified cell style. -=)) ((* extends cell_style *)) @@ -12,6 +12,6 @@ % Latex Article %=============================================================================== -((* block docclass *)) +((*- block docclass -*)) \documentclass[11pt]{article} ((* endblock docclass *)) diff --git a/nbconvert/templates/latex/base.tplx b/nbconvert/templates/latex/base.tplx index 74cd65e84..a879835e6 100644 --- a/nbconvert/templates/latex/base.tplx +++ b/nbconvert/templates/latex/base.tplx @@ -1,7 +1,8 @@ -((= Latex base template (must inherit) +((=- Latex base template (must inherit) This template builds upon the abstract template, adding common latex output -functions. Figures, data_text, -This template does not define a docclass, the inheriting class must define this.=)) +functions. Figures, data_text, +This template defines defines a default docclass, the inheriting class should +override this.-=)) ((*- extends 'document_contents.tplx' -*)) @@ -9,8 +10,8 @@ This template does not define a docclass, the inheriting class must define this. % Abstract overrides %=============================================================================== -((* block header *)) - ((* block docclass *))((* endblock docclass *)) +((*- block header -*)) + ((* block docclass *))\documentclass[11pt]{article}((* endblock docclass *)) ((* block packages *)) \usepackage[T1]{fontenc} diff --git a/nbconvert/templates/latex/skeleton/null.tplx b/nbconvert/templates/latex/skeleton/null.tplx index 398d7ed13..c9c380381 100644 --- a/nbconvert/templates/latex/skeleton/null.tplx +++ b/nbconvert/templates/latex/skeleton/null.tplx @@ -1,5 +1,5 @@ -((= Auto-generated template file, DO NOT edit directly! - To edit this file, please refer to ../../skeleton/README.md =)) +((=- Auto-generated template file, DO NOT edit directly! + To edit this file, please refer to ../../skeleton/README.md -=)) ((= diff --git a/nbconvert/templates/skeleton/Makefile b/nbconvert/templates/skeleton/Makefile index c02ac0fe7..65b9ade66 100644 --- a/nbconvert/templates/skeleton/Makefile +++ b/nbconvert/templates/skeleton/Makefile @@ -6,9 +6,9 @@ all: clean $(TPLS) # see http://flask.pocoo.org/snippets/55/ for more info ../latex/skeleton/%.tplx: %.tpl @echo 'generating tex equivalent of $^: $@' - @echo '((= Auto-generated template file, DO NOT edit directly!\n' \ + @echo '((=- Auto-generated template file, DO NOT edit directly!\n' \ ' To edit this file, please refer to ../../skeleton/README.md' \ - '=))\n\n' > $@ + '-=))\n\n' > $@ @sed \ -e 's/{%/((*/g' \ -e 's/%}/*))/g' \ From 2f28a26ef764160525ac2e0b596f7da92f1292ef Mon Sep 17 00:00:00 2001 From: Tyler Makaro Date: Tue, 30 Jul 2019 14:06:59 -0700 Subject: [PATCH 362/671] make the latex export easier to read by improving spacing. --- nbconvert/templates/latex/article.tplx | 2 +- nbconvert/templates/latex/base.tplx | 9 ++++----- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/nbconvert/templates/latex/article.tplx b/nbconvert/templates/latex/article.tplx index eb26195b4..b87a7ff18 100644 --- a/nbconvert/templates/latex/article.tplx +++ b/nbconvert/templates/latex/article.tplx @@ -14,4 +14,4 @@ ((*- block docclass -*)) \documentclass[11pt]{article} -((* endblock docclass *)) +((*- endblock docclass -*)) diff --git a/nbconvert/templates/latex/base.tplx b/nbconvert/templates/latex/base.tplx index a879835e6..edd5502e1 100644 --- a/nbconvert/templates/latex/base.tplx +++ b/nbconvert/templates/latex/base.tplx @@ -12,8 +12,8 @@ override this.-=)) ((*- block header -*)) ((* block docclass *))\documentclass[11pt]{article}((* endblock docclass *)) - - ((* block packages *)) + + ((* block packages -*)) \usepackage[T1]{fontenc} % Nicer default font (+ math font) than Computer Modern for most use cases \usepackage{mathpazo} @@ -177,8 +177,7 @@ override this.-=)) ((* endblock header *)) ((* block body *)) - \begin{document} - +\begin{document} ((* block predoc *)) ((* block maketitle *))\maketitle((* endblock maketitle *)) ((* block abstract *))((* endblock abstract *)) @@ -190,5 +189,5 @@ override this.-=)) ((* block postdoc *)) ((* block bibliography *))((* endblock bibliography *)) ((* endblock postdoc *)) - \end{document} +\end{document} ((* endblock body *)) From 1b90bf9fc66d4f7c0865233642c9436484fb0a64 Mon Sep 17 00:00:00 2001 From: Tyler Makaro Date: Tue, 30 Jul 2019 14:28:43 -0700 Subject: [PATCH 363/671] fix #1059 --- nbconvert/exporters/templateexporter.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/nbconvert/exporters/templateexporter.py b/nbconvert/exporters/templateexporter.py index 4a3fff78b..dfcd49399 100644 --- a/nbconvert/exporters/templateexporter.py +++ b/nbconvert/exporters/templateexporter.py @@ -319,6 +319,8 @@ def from_notebook_node(self, nb, resources=None, **kw): # Top level variables are passed to the template_exporter here. output = self.template.render(nb=nb_copy, resources=resources) + if resources.get('strip_preceding_newlines', True): + output = output.lstrip('\r\n') return output, resources def _register_filter(self, environ, name, jinja_filter): From 26faf4a45943fa3fa99b6076fc5fb4ee162d1b1f Mon Sep 17 00:00:00 2001 From: Tyler Makaro Date: Tue, 30 Jul 2019 15:44:57 -0700 Subject: [PATCH 364/671] remove resource for striping of starting newlines --- nbconvert/exporters/templateexporter.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/nbconvert/exporters/templateexporter.py b/nbconvert/exporters/templateexporter.py index dfcd49399..ad68decf0 100644 --- a/nbconvert/exporters/templateexporter.py +++ b/nbconvert/exporters/templateexporter.py @@ -319,8 +319,7 @@ def from_notebook_node(self, nb, resources=None, **kw): # Top level variables are passed to the template_exporter here. output = self.template.render(nb=nb_copy, resources=resources) - if resources.get('strip_preceding_newlines', True): - output = output.lstrip('\r\n') + output = output.lstrip('\r\n') return output, resources def _register_filter(self, environ, name, jinja_filter): From 386cdf1e1427db151858416dec52707ddc553594 Mon Sep 17 00:00:00 2001 From: Tyler Makaro Date: Tue, 30 Jul 2019 16:35:56 -0700 Subject: [PATCH 365/671] remove extractoutputpreprocessor changes --- nbconvert/exporters/latex.py | 3 +-- nbconvert/nbconvertapp.py | 3 +-- nbconvert/preprocessors/extractoutput.py | 15 ++------------- 3 files changed, 4 insertions(+), 17 deletions(-) diff --git a/nbconvert/exporters/latex.py b/nbconvert/exporters/latex.py index ed5ddc78f..eae351308 100644 --- a/nbconvert/exporters/latex.py +++ b/nbconvert/exporters/latex.py @@ -56,8 +56,7 @@ def default_config(self): 'display_data_priority' : ['text/latex', 'application/pdf', 'image/png', 'image/jpeg', 'image/svg+xml', 'text/markdown', 'text/plain'] }, 'ExtractOutputPreprocessor': { - 'enabled':True, - 'escape_dir_spaces':True + 'enabled':True }, 'SVG2PDFPreprocessor': { 'enabled':True diff --git a/nbconvert/nbconvertapp.py b/nbconvert/nbconvertapp.py index 9809db704..0a6a7f865 100755 --- a/nbconvert/nbconvertapp.py +++ b/nbconvert/nbconvertapp.py @@ -172,8 +172,7 @@ def _classes_default(self): output_files_dir = Unicode('{notebook_name}_files', help='''Directory to copy extra files (figures) to. '{notebook_name}' in the string will be converted to notebook - basename. Spaces will be replaced with underscores when using - the latex/pdf exporters.''' + basename.''' ).tag(config=True) examples = Unicode(u""" diff --git a/nbconvert/preprocessors/extractoutput.py b/nbconvert/preprocessors/extractoutput.py index 1066658e4..42acbb2cf 100755 --- a/nbconvert/preprocessors/extractoutput.py +++ b/nbconvert/preprocessors/extractoutput.py @@ -12,7 +12,7 @@ import json from mimetypes import guess_extension -from traitlets import Unicode, Set, Bool +from traitlets import Unicode, Set from .base import Preprocessor if sys.version_info < (3,): @@ -54,11 +54,6 @@ class ExtractOutputPreprocessor(Preprocessor): {'image/png', 'image/jpeg', 'image/svg+xml', 'application/pdf'} ).tag(config=True) - escape_dir_spaces = Bool( - False, help="If True, spaces will be replaced with underscores in the " - "output directory." - ).tag(config=True) - def preprocess_cell(self, cell, resources, cell_index): """ Apply a transformation on each cell, @@ -79,13 +74,7 @@ def preprocess_cell(self, cell, resources, cell_index): #has been specified unique_key = resources.get('unique_key', 'output') output_files_dir = resources.get('output_files_dir', None) - if self.escape_dir_spaces: - unique_key = unique_key.replace(' ', '_') - resources['unique_key'] = unique_key - if output_files_dir is not None: - output_files_dir = output_files_dir.replace(' ', '_') - resources['output_files_dir'] = output_files_dir - + #Make sure outputs key exists if not isinstance(resources['outputs'], dict): From 46d2b1695772bc649f0626cd29d3225c29cd4375 Mon Sep 17 00:00:00 2001 From: Tyler Makaro Date: Tue, 30 Jul 2019 16:36:55 -0700 Subject: [PATCH 366/671] remove space --- nbconvert/preprocessors/extractoutput.py | 1 - 1 file changed, 1 deletion(-) diff --git a/nbconvert/preprocessors/extractoutput.py b/nbconvert/preprocessors/extractoutput.py index 42acbb2cf..eb8c8594e 100755 --- a/nbconvert/preprocessors/extractoutput.py +++ b/nbconvert/preprocessors/extractoutput.py @@ -75,7 +75,6 @@ def preprocess_cell(self, cell, resources, cell_index): unique_key = resources.get('unique_key', 'output') output_files_dir = resources.get('output_files_dir', None) - #Make sure outputs key exists if not isinstance(resources['outputs'], dict): resources['outputs'] = {} From f4a412e28f834b2f9151015ad619988e5d66afc8 Mon Sep 17 00:00:00 2001 From: Tyler Makaro Date: Tue, 30 Jul 2019 17:46:46 -0700 Subject: [PATCH 367/671] fix spaces in filenames --- nbconvert/templates/latex/base.tplx | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/nbconvert/templates/latex/base.tplx b/nbconvert/templates/latex/base.tplx index 74cd65e84..ca677d252 100644 --- a/nbconvert/templates/latex/base.tplx +++ b/nbconvert/templates/latex/base.tplx @@ -49,7 +49,15 @@ This template does not define a docclass, the inheriting class must define this. \usepackage[utf8x]{inputenc} % Allow utf-8 characters in the tex document \usepackage{fancyvrb} % verbatim replacement that allows latex \usepackage{grffile} % extends the file name processing of package graphics - % to support a larger range + % to support a larger range + \makeatletter % fix for grffile with XeLaTeX + \def\Gread@@xetex#1{% + \IfFileExists{"\Gin@base".bb}% + {\Gread@eps{\Gin@base.bb}}% + {\Gread@@xetex@aux#1}% + } + \makeatother + % The hyperref package gives us a pdf with properly built % internal navigation ('pdf bookmarks' for the table of contents, % internal cross-reference links, web links for URLs, etc.) From d9a55b2674b04c798a6546db12d49518e6471b10 Mon Sep 17 00:00:00 2001 From: Tyler Makaro Date: Tue, 30 Jul 2019 18:39:32 -0700 Subject: [PATCH 368/671] fix ansi2latex --- nbconvert/filters/ansi.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/nbconvert/filters/ansi.py b/nbconvert/filters/ansi.py index 55bc47acb..039e84130 100644 --- a/nbconvert/filters/ansi.py +++ b/nbconvert/filters/ansi.py @@ -189,6 +189,10 @@ def _ansi2anything(text, converter): numbers = [] out = [] + fix_ending_space = (converter == _latexconverter) and text.endswith('\n') + if fix_ending_space: + text = text[:-1] + while text: m = _ANSI_RE.search(text) if m: @@ -258,7 +262,11 @@ def _ansi2anything(text, converter): bg = n - 100 + 8 else: pass # Unknown codes are ignored - return ''.join(out) + + output = ''.join(out) + if fix_ending_space: + output += '\n' + return output def _get_extended_color(numbers): From 57486e715afb65d603e4031d407b9476706d800e Mon Sep 17 00:00:00 2001 From: Tyler Makaro Date: Tue, 30 Jul 2019 18:51:34 -0700 Subject: [PATCH 369/671] refactor ansi2latex --- nbconvert/filters/ansi.py | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/nbconvert/filters/ansi.py b/nbconvert/filters/ansi.py index 039e84130..c7b9d95e3 100644 --- a/nbconvert/filters/ansi.py +++ b/nbconvert/filters/ansi.py @@ -71,7 +71,13 @@ def ansi2latex(text): Text containing ANSI colors to convert to LaTeX """ - return _ansi2anything(text, _latexconverter) + fix_ending_space = text.endswith('\n') + if fix_ending_space: + text = text[:-1] + text = _ansi2anything(text, _latexconverter) + if fix_ending_space: + text += '\n' + return text def _htmlconverter(fg, bg, bold, underline, inverse): @@ -189,10 +195,6 @@ def _ansi2anything(text, converter): numbers = [] out = [] - fix_ending_space = (converter == _latexconverter) and text.endswith('\n') - if fix_ending_space: - text = text[:-1] - while text: m = _ANSI_RE.search(text) if m: @@ -263,10 +265,7 @@ def _ansi2anything(text, converter): else: pass # Unknown codes are ignored - output = ''.join(out) - if fix_ending_space: - output += '\n' - return output + return ''.join(out) def _get_extended_color(numbers): From 785980647cfcd55d22d1cd57f689a619dc46e7d3 Mon Sep 17 00:00:00 2001 From: Tyler Makaro Date: Tue, 30 Jul 2019 18:53:22 -0700 Subject: [PATCH 370/671] refactor whitespace --- nbconvert/filters/ansi.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/nbconvert/filters/ansi.py b/nbconvert/filters/ansi.py index c7b9d95e3..ce16edd70 100644 --- a/nbconvert/filters/ansi.py +++ b/nbconvert/filters/ansi.py @@ -74,7 +74,9 @@ def ansi2latex(text): fix_ending_space = text.endswith('\n') if fix_ending_space: text = text[:-1] + text = _ansi2anything(text, _latexconverter) + if fix_ending_space: text += '\n' return text @@ -264,7 +266,6 @@ def _ansi2anything(text, converter): bg = n - 100 + 8 else: pass # Unknown codes are ignored - return ''.join(out) From 6c5ee76d344e64ce2ede83abb0855449c8522579 Mon Sep 17 00:00:00 2001 From: Tyler Makaro Date: Wed, 31 Jul 2019 09:12:30 -0700 Subject: [PATCH 371/671] strip the last newline off all stream blocks --- nbconvert/filters/ansi.py | 10 ++-------- nbconvert/templates/latex/style_jupyter.tplx | 2 +- 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/nbconvert/filters/ansi.py b/nbconvert/filters/ansi.py index ce16edd70..849e058ba 100644 --- a/nbconvert/filters/ansi.py +++ b/nbconvert/filters/ansi.py @@ -71,15 +71,9 @@ def ansi2latex(text): Text containing ANSI colors to convert to LaTeX """ - fix_ending_space = text.endswith('\n') - if fix_ending_space: + if text.endswith('\n'): text = text[:-1] - - text = _ansi2anything(text, _latexconverter) - - if fix_ending_space: - text += '\n' - return text + return _ansi2anything(text, _latexconverter) def _htmlconverter(fg, bg, bold, underline, inverse): diff --git a/nbconvert/templates/latex/style_jupyter.tplx b/nbconvert/templates/latex/style_jupyter.tplx index 3d7a08580..0769acfd8 100644 --- a/nbconvert/templates/latex/style_jupyter.tplx +++ b/nbconvert/templates/latex/style_jupyter.tplx @@ -139,7 +139,7 @@ ((* block stream *)) \begin{Verbatim}[commandchars=\\\{\}] -((( output.text | wrap_text(charlim) | escape_latex | ansi2latex -))) +((( output.text | wrap_text(charlim) | escape_latex | ansi2latex ))) \end{Verbatim} ((* endblock stream *)) From 5797e3c8059ce60b1dc0312c98923f6783b7e5da Mon Sep 17 00:00:00 2001 From: Tyler Makaro Date: Wed, 31 Jul 2019 10:01:07 -0700 Subject: [PATCH 372/671] add strip_trailing_newline filter --- nbconvert/exporters/templateexporter.py | 1 + nbconvert/filters/ansi.py | 2 -- nbconvert/filters/strings.py | 9 +++++++++ nbconvert/templates/latex/style_jupyter.tplx | 2 +- 4 files changed, 11 insertions(+), 3 deletions(-) diff --git a/nbconvert/exporters/templateexporter.py b/nbconvert/exporters/templateexporter.py index 4a3fff78b..4430a402f 100644 --- a/nbconvert/exporters/templateexporter.py +++ b/nbconvert/exporters/templateexporter.py @@ -58,6 +58,7 @@ 'get_metadata': filters.get_metadata, 'convert_pandoc': filters.convert_pandoc, 'json_dumps': json.dumps, + 'strip_trailing_newline': filters.strip_trailing_newline, } class ExtensionTolerantLoader(BaseLoader): diff --git a/nbconvert/filters/ansi.py b/nbconvert/filters/ansi.py index 849e058ba..55bc47acb 100644 --- a/nbconvert/filters/ansi.py +++ b/nbconvert/filters/ansi.py @@ -71,8 +71,6 @@ def ansi2latex(text): Text containing ANSI colors to convert to LaTeX """ - if text.endswith('\n'): - text = text[:-1] return _ansi2anything(text, _latexconverter) diff --git a/nbconvert/filters/strings.py b/nbconvert/filters/strings.py index e5bad48ac..898dd1863 100755 --- a/nbconvert/filters/strings.py +++ b/nbconvert/filters/strings.py @@ -39,6 +39,7 @@ 'add_prompts', 'ascii_only', 'prevent_list_blocks', + 'strip_trailing_newline', ] @@ -241,3 +242,11 @@ def prevent_list_blocks(s): out = re.sub('(^\s*)\+', '\\1\+', out) out = re.sub('(^\s*)\*', '\\1\*', out) return out + +def strip_trailing_newline(text): + """ + Strips a newline from the end of text. + """ + if text.endswith('\n'): + text = text[:-1] + return text diff --git a/nbconvert/templates/latex/style_jupyter.tplx b/nbconvert/templates/latex/style_jupyter.tplx index 0769acfd8..66699287c 100644 --- a/nbconvert/templates/latex/style_jupyter.tplx +++ b/nbconvert/templates/latex/style_jupyter.tplx @@ -139,7 +139,7 @@ ((* block stream *)) \begin{Verbatim}[commandchars=\\\{\}] -((( output.text | wrap_text(charlim) | escape_latex | ansi2latex ))) +((( output.text | wrap_text(charlim) | escape_latex | strip_trailing_newline | ansi2latex ))) \end{Verbatim} ((* endblock stream *)) From 8b3ef736056c3a989074e768fe2f514e2b2fa7ee Mon Sep 17 00:00:00 2001 From: KrokodileDandy Date: Thu, 1 Aug 2019 12:06:56 +0200 Subject: [PATCH 373/671] Requested change to specify fontenc or fontspec for xelatex or pdflatex users. --- nbconvert/templates/latex/base.tplx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/nbconvert/templates/latex/base.tplx b/nbconvert/templates/latex/base.tplx index 64f13d6da..5281c63a4 100644 --- a/nbconvert/templates/latex/base.tplx +++ b/nbconvert/templates/latex/base.tplx @@ -13,7 +13,13 @@ This template does not define a docclass, the inheriting class must define this. ((* block docclass *))((* endblock docclass *)) ((* block packages *)) - \usepackage{fontspec} + \usepackage{iftex} + \ifPDFTeX + \usepackage[T1]{fontenc} + \usepackage{mathpazo} + \else + \usepackage{fontspec} + \fi % Basic figure setup, for now with no caption control since it's done % automatically by Pandoc (which extracts ![](path) syntax from Markdown). From 5ab842c106f95b42ae23cdf03ca9321545423a8d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20F=C3=BChr?= <40100920+KrokodileDandy@users.noreply.github.com> Date: Thu, 1 Aug 2019 15:41:50 +0200 Subject: [PATCH 374/671] iftex-support The package `texlive-generic-extra` allows the usage of the `iftex` package. --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index ccc955f66..9cb9c6768 100644 --- a/.travis.yml +++ b/.travis.yml @@ -28,6 +28,7 @@ addons: - texlive-xetex # latex to pdf converter - inkscape # for svgs in pdf output - lmodern # latex package + - texlive-generic-extra install: - wget https://github.com/jgm/pandoc/releases/download/2.7/pandoc-2.7-1-amd64.deb && sudo dpkg -i pandoc-2.7-1-amd64.deb - pip install --upgrade setuptools pip pytest From 6fd7173988dd46f69955003c26810ab1f985196a Mon Sep 17 00:00:00 2001 From: Tyler Makaro Date: Thu, 1 Aug 2019 17:03:38 -0700 Subject: [PATCH 375/671] Remove whitespace gobble --- nbconvert/templates/latex/base.tplx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nbconvert/templates/latex/base.tplx b/nbconvert/templates/latex/base.tplx index edd5502e1..9ce970946 100644 --- a/nbconvert/templates/latex/base.tplx +++ b/nbconvert/templates/latex/base.tplx @@ -13,7 +13,7 @@ override this.-=)) ((*- block header -*)) ((* block docclass *))\documentclass[11pt]{article}((* endblock docclass *)) - ((* block packages -*)) + ((* block packages *)) \usepackage[T1]{fontenc} % Nicer default font (+ math font) than Computer Modern for most use cases \usepackage{mathpazo} From a31cf2cfae62bd57f1cb7edf0ea566a5303a15bb Mon Sep 17 00:00:00 2001 From: Min RK Date: Wed, 7 Aug 2019 12:32:53 +0200 Subject: [PATCH 376/671] fix some deprecated regex escapes use r' prefix for raw strings --- nbconvert/filters/strings.py | 8 ++++---- nbconvert/filters/tests/test_markdown.py | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/nbconvert/filters/strings.py b/nbconvert/filters/strings.py index 898dd1863..3cef02c26 100755 --- a/nbconvert/filters/strings.py +++ b/nbconvert/filters/strings.py @@ -237,10 +237,10 @@ def prevent_list_blocks(s): """ Prevent presence of enumerate or itemize blocks in latex headings cells """ - out = re.sub('(^\s*\d*)\.', '\\1\.', s) - out = re.sub('(^\s*)\-', '\\1\-', out) - out = re.sub('(^\s*)\+', '\\1\+', out) - out = re.sub('(^\s*)\*', '\\1\*', out) + out = re.sub(r'(^\s*\d*)\.', r'\1\.', s) + out = re.sub(r'(^\s*)\-', r'\1\-', out) + out = re.sub(r'(^\s*)\+', r'\1\+', out) + out = re.sub(r'(^\s*)\*', r'\1\*', out) return out def strip_trailing_newline(text): diff --git a/nbconvert/filters/tests/test_markdown.py b/nbconvert/filters/tests/test_markdown.py index b1e8c6ba6..4ab6ce0f0 100644 --- a/nbconvert/filters/tests/test_markdown.py +++ b/nbconvert/filters/tests/test_markdown.py @@ -159,7 +159,7 @@ def test_markdown2html_math(self): for case in cases: result = markdown2html(case) # find the equation in the generated texts - search_result = re.search("\$.*\$", result, re.DOTALL) + search_result = re.search(r"\$.*\$", result, re.DOTALL) if search_result is None: search_result = re.search( "\\\\begin\\{equation.*\\}.*\\\\end\\{equation.*\\}", From a38e1366bc221bed209e0f5746f29e319bd7668e Mon Sep 17 00:00:00 2001 From: Min RK Date: Wed, 7 Aug 2019 12:39:36 +0200 Subject: [PATCH 377/671] deprecated use of traitlets trait types should be instantiated --- nbconvert/preprocessors/sanitize.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nbconvert/preprocessors/sanitize.py b/nbconvert/preprocessors/sanitize.py index e91d39eee..3f4ef53cc 100644 --- a/nbconvert/preprocessors/sanitize.py +++ b/nbconvert/preprocessors/sanitize.py @@ -27,13 +27,13 @@ class SanitizeHTML(Preprocessor): help="Allowed HTML tag attributes", ) tags = List( - Unicode, + Unicode(), config=True, default_value=ALLOWED_TAGS, help="List of HTML tags to allow", ) styles = List( - Unicode, + Unicode(), config=True, default_value=ALLOWED_STYLES, help="Allowed CSS styles if """ % (env.loader.get_source(env, name)[0]) + return jinja2.Markup(code) + + def resources_include_js(name): + env = self.environment + code = """""" % (env.loader.get_source(env, name)[0]) + return jinja2.Markup(code) + + def resources_include_url(name): + env = self.environment + mime_type, encoding = mimetypes.guess_type(name) + try: + # we try to load via the jinja loader, but that tries to load + # as (encoded) text + data = env.loader.get_source(env, name)[0].encode('utf8') + except UnicodeDecodeError: + # if that fails (for instance a binary file, png or ttf) + # we mimic jinja2 + pieces = split_template_path(name) + searchpaths = self.get_template_paths() + for searchpath in searchpaths: + filename = os.path.join(searchpath, *pieces) + print(filename, os.path.exists(filename)) + if os.path.exists(filename): + with open(filename, "rb") as f: + data = f.read() + break + else: + raise ValueError("No file %r found in %r" % (name, searchpaths)) + data = base64.b64encode(data) + data = data.replace(b'\n', b'').decode('ascii') + src = 'data:{mime_type};base64,{data}'.format(mime_type=mime_type, data=data) + return jinja2.Markup(src) + resources = super(HTMLExporter, self)._init_resources(resources) + resources['theme'] = self.theme + resources['include_css'] = resources_include_css + resources['include_js'] = resources_include_js + resources['include_url'] = resources_include_url + return resources diff --git a/nbconvert/exporters/latex.py b/nbconvert/exporters/latex.py index b9f82eb4e..21793d27a 100644 --- a/nbconvert/exporters/latex.py +++ b/nbconvert/exporters/latex.py @@ -27,25 +27,14 @@ class LatexExporter(TemplateExporter): def _file_extension_default(self): return '.tex' - @default('template_file') - def _template_file_default(self): - return 'article.tplx' - - # Latex constants - @default('default_template_path') - def _default_template_path_default(self): - return os.path.join("..", "templates", "latex") - - @default('template_skeleton_path') - def _template_skeleton_path_default(self): - return os.path.join("..", "templates", "latex", "skeleton") @default('template_data_paths') def _template_data_paths_default(self): return jupyter_path("nbconvert", "templates", "latex") - - #Extension that the template files use. - template_extension = Unicode(".tplx").tag(config=True) + + @default('template_name') + def _template_name_default(self): + return 'latex' output_mimetype = 'text/latex' diff --git a/nbconvert/exporters/markdown.py b/nbconvert/exporters/markdown.py index b810d3181..24d357f95 100644 --- a/nbconvert/exporters/markdown.py +++ b/nbconvert/exporters/markdown.py @@ -19,9 +19,9 @@ class MarkdownExporter(TemplateExporter): def _file_extension_default(self): return '.md' - @default('template_file') - def _template_file_default(self): - return 'markdown.tpl' + @default('template_name') + def _template_name_default(self): + return 'markdown' output_mimetype = 'text/markdown' diff --git a/nbconvert/exporters/pdf.py b/nbconvert/exporters/pdf.py index a84824362..33619d1a9 100644 --- a/nbconvert/exporters/pdf.py +++ b/nbconvert/exporters/pdf.py @@ -73,6 +73,11 @@ class PDFExporter(LatexExporter): def _file_extension_default(self): return '.pdf' + + @default('template_extension') + def _template_extension_default(self): + return '.tex.j2' + def run_command(self, command_list, filename, count, log_function, raise_on_failure=None): """Run command_list count times. diff --git a/nbconvert/exporters/python.py b/nbconvert/exporters/python.py index 642003632..aa833c813 100644 --- a/nbconvert/exporters/python.py +++ b/nbconvert/exporters/python.py @@ -16,8 +16,8 @@ class PythonExporter(TemplateExporter): def _file_extension_default(self): return '.py' - @default('template_file') - def _template_file_default(self): - return 'python.tpl' + @default('template_name') + def _template_name_default(self): + return 'python' output_mimetype = 'text/x-python' diff --git a/nbconvert/exporters/rst.py b/nbconvert/exporters/rst.py index 568144ba9..b27b9535c 100644 --- a/nbconvert/exporters/rst.py +++ b/nbconvert/exporters/rst.py @@ -18,9 +18,9 @@ class RSTExporter(TemplateExporter): def _file_extension_default(self): return '.rst' - @default('template_file') - def _template_file_default(self): - return 'rst.tpl' + @default('template_name') + def _template_name_default(self): + return 'rst' output_mimetype = 'text/restructuredtext' export_from_notebook = "reST" diff --git a/nbconvert/exporters/script.py b/nbconvert/exporters/script.py index 0875df60f..c9986289d 100644 --- a/nbconvert/exporters/script.py +++ b/nbconvert/exporters/script.py @@ -18,7 +18,11 @@ class ScriptExporter(TemplateExporter): @default('template_file') def _template_file_default(self): - return 'script.tpl' + return 'script.j2' + + @default('template_name') + def _template_name_default(self): + return 'script' def _get_language_exporter(self, lang_name): """Find an exporter for the language name from notebook metadata. diff --git a/nbconvert/exporters/slides.py b/nbconvert/exporters/slides.py index 500eef854..1f9333068 100644 --- a/nbconvert/exporters/slides.py +++ b/nbconvert/exporters/slides.py @@ -77,6 +77,14 @@ class SlidesExporter(HTMLExporter): export_from_notebook = "Reveal.js slides" + @default('template_name') + def _template_name_default(self): + return 'reveal' + + template_name = Unicode('reveal', + help="Name of the template to use" + ).tag(config=True, affects_template=True) + reveal_url_prefix = Unicode( help="""The URL prefix for reveal.js (version 3.x). This defaults to the reveal CDN, but can be any url pointing to a copy @@ -160,9 +168,9 @@ def _reveal_url_prefix_default(self): def _file_extension_default(self): return '.slides.html' - @default('template_file') - def _template_file_default(self): - return 'slides_reveal.tpl' + @default('template_extension') + def _template_extension_default(self): + return '.html.j2' output_mimetype = 'text/html' diff --git a/nbconvert/exporters/templateexporter.py b/nbconvert/exporters/templateexporter.py index 834480ad7..81b6d0930 100644 --- a/nbconvert/exporters/templateexporter.py +++ b/nbconvert/exporters/templateexporter.py @@ -11,6 +11,7 @@ import uuid import json +from jupyter_core.paths import jupyter_path from traitlets import HasTraits, Unicode, List, Dict, Bool, default, observe from traitlets.config import Config from traitlets.utils.importstring import import_item @@ -28,6 +29,10 @@ # Jinja2 extensions to load. JINJA_EXTENSIONS = ['jinja2.ext.loopcontrols'] +ROOT = os.path.dirname(__file__) +DEV_MODE = os.path.exists(os.path.join(ROOT, '../../setup.py')) and os.path.exists(os.path.join(ROOT, '../../share')) + + default_filters = { 'indent': filters.indent, 'markdown2html': filters.markdown2html, @@ -61,6 +66,29 @@ 'strip_trailing_newline': filters.strip_trailing_newline, } + +# copy of https://github.com/jupyter/jupyter_server/blob/b62458a7f5ad6b5246d2f142258dedaa409de5d9/jupyter_server/config_manager.py#L19 +def recursive_update(target, new): + """Recursively update one dictionary using another. + None values will delete their keys. + """ + for k, v in new.items(): + if isinstance(v, dict): + if k not in target: + target[k] = {} + recursive_update(target[k], v) + if not target[k]: + # Prune empty subdicts + del target[k] + + elif v is None: + target.pop(k, None) + + else: + target[k] = v + return target # return for convenience + + class ExtensionTolerantLoader(BaseLoader): """A template loader which optionally adds a given extension when searching. @@ -139,7 +167,10 @@ def default_config(self): c.merge(super(TemplateExporter, self).default_config) return c - template_file = Unicode( + template_name = Unicode(help="Name of the template to use" + ).tag(config=True, affects_template=True) + + template_file = Unicode(None, allow_none=True, help="Name of the template file to use" ).tag(config=True, affects_template=True) @@ -165,27 +196,20 @@ def _template_file_changed(self, change): @default('template_file') def _template_file_default(self): - return self.default_template + if self.template_extension: + return 'index' + self.template_extension @observe('raw_template') def _raw_template_changed(self, change): if not change['new']: - self.template_file = self.default_template or self._last_template_file + self.template_file = self._last_template_file self._invalidate_template_cache() - default_template = Unicode(u'').tag(affects_template=True) - template_path = List(['.']).tag(config=True, affects_environment=True) - default_template_path = Unicode( - os.path.join("..", "templates"), - help="Path where the template files are located." - ).tag(affects_environment=True) + #Extension that the template files use. + template_extension = Unicode().tag(config=True, affects_environment=True) - template_skeleton_path = Unicode( - os.path.join("..", "templates", "skeleton"), - help="Path where the template skeleton files are located.", - ).tag(affects_environment=True) template_data_paths = List( jupyter_path('nbconvert','templates'), @@ -194,6 +218,17 @@ def _raw_template_changed(self, change): #Extension that the template files use. template_extension = Unicode(".tpl").tag(config=True, affects_environment=True) + @default('template_extension') + def _template_extension_default(self): + if self.file_extension: + return self.file_extension + ".j2" + else: + return self.file_extension + + @default('template_file') + def _template_file_default(self): + if self.template_extension: + return 'index' + self.template_extension exclude_input = Bool(False, help = "This allows you to exclude code cell inputs from all templates if set to True." @@ -398,19 +433,8 @@ def _create_environment(self): """ Create the Jinja templating environment. """ - here = os.path.dirname(os.path.realpath(__file__)) - - additional_paths = self.template_data_paths - for path in additional_paths: - try: - ensure_dir_exists(path, mode=0o700) - except OSError: - pass - - paths = self.template_path + \ - additional_paths + \ - [os.path.join(here, self.default_template_path), - os.path.join(here, self.template_skeleton_path)] + paths = self.get_template_paths() + self.log.info('template paths:\n\t%s', '\n\t'.join(paths)) loaders = self.extra_loaders + [ ExtensionTolerantLoader(FileSystemLoader(paths), self.template_extension), @@ -433,3 +457,74 @@ def _create_environment(self): self._register_filter(environment, key, user_filter) return environment + + def get_template_paths(self, prune=False, root_dirs=None): + full_paths = [] + paths = list(self.template_path) + root_dirs = self.get_prefix_root_dirs() + template_names = self.get_template_names() + for template_name in template_names: + for root_dir in root_dirs: + base_dir = os.path.join(root_dir, 'nbconvert', 'templates') + path = os.path.join(base_dir, template_name) + if not prune or os.path.exists(path): + paths.append(path) + + for root_dir in root_dirs: + # we include root_dir for when we want to be very explicit, e.g. + # {% extends 'nbconvert/templates/classic/base.html' %} + paths.append(root_dir) + # we include base_dir for when we want to be explicit, but less than root_dir, e.g. + # {% extends 'classic/base.html' %} + base_dir = os.path.join(root_dir, 'nbconvert', 'templates') + paths.append(base_dir) + + additional_paths = self.template_data_paths + for path in additional_paths: + try: + ensure_dir_exists(path, mode=0o700) + except OSError: + pass + + + return additional_paths + paths + + def get_template_names(self): + # finds a list of template name where each successive template name is the base template + template_names = [] + root_dirs = self.get_prefix_root_dirs() + template_name = self.template_name + merged_conf = {} # the configuration once all conf files are merged + while template_name is not None: + template_names.append(template_name) + conf = {} + found_at_least_one = False + for root_dir in root_dirs: + template_dir = os.path.join(root_dir, 'nbconvert', 'templates', template_name) + if os.path.exists(template_dir): + found_at_least_one = True + conf_file = os.path.join(template_dir, 'conf.json') + if os.path.exists(conf_file): + with open(conf_file) as f: + conf = recursive_update(json.load(f), conf) + if not found_at_least_one: + paths = "\n\t".join(root_dirs) + raise ValueError('No template sub-directory with name %r found in the following paths:\n\t%s' % (template_name, paths)) + merged_conf = recursive_update(dict(conf), merged_conf) + template_name = conf.get('base_template') + conf = merged_conf + mimetypes = [mimetype for mimetype, enabled in conf.get('mimetypes', {}).items() if enabled] + if self.output_mimetype and self.output_mimetype not in mimetypes: + supported_mimetypes = '\n\t'.join(mimetypes) + raise ValueError('Unsupported mimetype %r for template %r, mimetypes supported are: \n\t%s' %\ + (self.output_mimetype, self.template_name, supported_mimetypes)) + return template_names + + def get_prefix_root_dirs(self): + # We look at the usual jupyter locations, and for development purposes also + # relative to the package directory (first entry, meaning with highest precedence) + root_dirs = [] + if DEV_MODE: + root_dirs.append(os.path.abspath(os.path.join(ROOT, '..', '..', 'share', 'jupyter'))) + root_dirs.extend(jupyter_path()) + return root_dirs diff --git a/nbconvert/exporters/tests/test_html.py b/nbconvert/exporters/tests/test_html.py index 507a713a1..ef1137bd0 100644 --- a/nbconvert/exporters/tests/test_html.py +++ b/nbconvert/exporters/tests/test_html.py @@ -33,26 +33,26 @@ def test_export(self): assert len(output) > 0 - def test_export_basic(self): + def test_export_classic(self): """ - Can a HTMLExporter export using the 'basic' template? + Can a HTMLExporter export using the 'classic' template? """ - (output, resources) = HTMLExporter(template_file='basic').from_filename(self._get_notebook()) + (output, resources) = HTMLExporter(template_name='classic').from_filename(self._get_notebook()) assert len(output) > 0 - def test_export_full(self): + def test_export_notebook(self): """ - Can a HTMLExporter export using the 'full' template? + Can a HTMLExporter export using the 'lab' template? """ - (output, resources) = HTMLExporter(template_file='full').from_filename(self._get_notebook()) + (output, resources) = HTMLExporter(template_name='lab').from_filename(self._get_notebook()) assert len(output) > 0 def test_prompt_number(self): """ Does HTMLExporter properly format input and output prompts? """ - (output, resources) = HTMLExporter(template_file='full').from_filename( + (output, resources) = HTMLExporter(template_name='lab').from_filename( self._get_notebook(nb_name="prompt_numbers.ipynb")) in_regex = r"In \[(.*)\]:" out_regex = r"Out\[(.*)\]:" @@ -74,7 +74,7 @@ def test_prompt_number(self): } } ) - exporter = HTMLExporter(config=no_prompt_conf, template_file='full') + exporter = HTMLExporter(config=no_prompt_conf, template_name='lab') (output, resources) = exporter.from_filename( self._get_notebook(nb_name="prompt_numbers.ipynb")) in_regex = r"In \[(.*)\]:" @@ -85,9 +85,9 @@ def test_prompt_number(self): def test_png_metadata(self): """ - Does HTMLExporter with the 'basic' template treat pngs with width/height metadata correctly? + Does HTMLExporter with the 'classic' template treat pngs with width/height metadata correctly? """ - (output, resources) = HTMLExporter(template_file='basic').from_filename( + (output, resources) = HTMLExporter(template_name='classic').from_filename( self._get_notebook(nb_name="pngmetadata.ipynb")) check_for_png = re.compile(r']*?)>') result = check_for_png.search(output) @@ -108,11 +108,11 @@ def test_javascript_output(self): ) ] ) - (output, resources) = HTMLExporter(template_file='basic').from_notebook_node(nb) + (output, resources) = HTMLExporter(template_name='classic').from_notebook_node(nb) self.assertIn('javascript_output', output) def test_attachments(self): - (output, resources) = HTMLExporter(template_file='basic').from_file( + (output, resources) = HTMLExporter(template_name='classic').from_file( self._get_notebook(nb_name='attachment.ipynb') ) check_for_png = re.compile(r']*?)>') @@ -137,6 +137,6 @@ def custom_highlight_code(source, language="python", metadata=None): filters = { "highlight_code": custom_highlight_code } - (output, resources) = HTMLExporter(template_file='basic', filters=filters).from_notebook_node(nb) + (output, resources) = HTMLExporter(template_name='classic', filters=filters).from_notebook_node(nb) self.assertTrue("ADDED_TEXT" in output) diff --git a/nbconvert/exporters/tests/test_latex.py b/nbconvert/exporters/tests/test_latex.py index 63e97d832..262bff8b9 100644 --- a/nbconvert/exporters/tests/test_latex.py +++ b/nbconvert/exporters/tests/test_latex.py @@ -53,23 +53,6 @@ def test_export_book(self): assert len(output) > 0 - @onlyif_cmds_exist('pandoc') - def test_export_basic(self): - """ - Can a LatexExporter export using 'article' template? - """ - (output, resources) = LatexExporter(template_file='article').from_filename(self._get_notebook()) - assert len(output) > 0 - - - @onlyif_cmds_exist('pandoc') - def test_export_article(self): - """ - Can a LatexExporter export using 'article' template? - """ - (output, resources) = LatexExporter(template_file='article').from_filename(self._get_notebook()) - assert len(output) > 0 - @onlyif_cmds_exist('pandoc') def test_very_long_cells(self): """ @@ -104,7 +87,7 @@ def test_very_long_cells(self): with open(nbfile, 'w') as f: write(nb, f, 4) - (output, resources) = LatexExporter(template_file='article').from_filename(nbfile) + (output, resources) = LatexExporter().from_filename(nbfile) assert len(output) > 0 @onlyif_cmds_exist('pandoc') @@ -133,7 +116,7 @@ def test_prompt_number_color_ipython(self): """ my_loader_tplx = DictLoader({'my_template': """ - ((* extends 'style_ipython.tplx' *)) + ((* extends 'style_ipython.tex.j2' *)) ((* block docclass *)) \documentclass[11pt]{article} @@ -184,7 +167,7 @@ def test_in_memory_template_tplx(self): # Loads in an in memory latex template (.tplx) using jinja2.DictLoader # creates a class that uses this template with the template_file argument # converts an empty notebook using this mechanism - my_loader_tplx = DictLoader({'my_template': "{%- extends 'article.tplx' -%}"}) + my_loader_tplx = DictLoader({'my_template': "{%- extends 'index' -%}"}) class MyExporter(LatexExporter): template_file = 'my_template' diff --git a/nbconvert/exporters/tests/test_slides.py b/nbconvert/exporters/tests/test_slides.py index f7873c159..5890d99d0 100644 --- a/nbconvert/exporters/tests/test_slides.py +++ b/nbconvert/exporters/tests/test_slides.py @@ -33,7 +33,7 @@ def test_export_reveal(self): """ Can a SlidesExporter export using the 'reveal' template? """ - (output, resources) = SlidesExporter(template_file='slides_reveal').from_filename(self._get_notebook()) + (output, resources) = SlidesExporter().from_filename(self._get_notebook()) assert len(output) > 0 def build_notebook(self): diff --git a/nbconvert/exporters/tests/test_templateexporter.py b/nbconvert/exporters/tests/test_templateexporter.py index 0a57b6ebe..4f3c2f40a 100644 --- a/nbconvert/exporters/tests/test_templateexporter.py +++ b/nbconvert/exporters/tests/test_templateexporter.py @@ -22,7 +22,7 @@ import pytest -raw_template = """{%- extends 'rst.tpl' -%} +raw_template = """{%- extends 'index.rst.j2' -%} {%- block in_prompt -%} blah {%- endblock in_prompt -%} @@ -140,7 +140,7 @@ def test_raw_template_attr(self): class AttrExporter(TemplateExporter): raw_template = raw_template - exporter_attr = AttrExporter() + exporter_attr = AttrExporter(template_name='rst') output_attr, _ = exporter_attr.from_notebook_node(nb) assert "blah" in output_attr @@ -163,7 +163,7 @@ def __init__(self, *args, **kwargs): output_init, _ = exporter_init.from_notebook_node(nb) assert "blah" in output_init exporter_init.raw_template = '' - assert exporter_init.template_file == "rst.tpl" + assert exporter_init.template_file == "index.rst.j2" output_init, _ = exporter_init.from_notebook_node(nb) assert "blah" not in output_init @@ -178,19 +178,19 @@ def test_raw_template_dynamic_attr(self): nb.cells.append(v4.new_code_cell("some_text")) class AttrDynamicExporter(TemplateExporter): - @default('template_file') + @default('default_template_file') def _template_file_default(self): - return "rst.tpl" + return "index.rst.j2" @default('raw_template') def _raw_template_default(self): return raw_template - exporter_attr_dynamic = AttrDynamicExporter() + exporter_attr_dynamic = AttrDynamicExporter(template_name='rst') output_attr_dynamic, _ = exporter_attr_dynamic.from_notebook_node(nb) assert "blah" in output_attr_dynamic exporter_attr_dynamic.raw_template = '' - assert exporter_attr_dynamic.template_file == "rst.tpl" + assert exporter_attr_dynamic.template_file == "index.rst.j2" output_attr_dynamic, _ = exporter_attr_dynamic.from_notebook_node(nb) assert "blah" not in output_attr_dynamic @@ -209,15 +209,15 @@ class AttrDynamicExporter(TemplateExporter): def _raw_template_default(self): return raw_template - @default('template_file') + @default('default_template_file') def _template_file_default(self): - return "rst.tpl" + return 'index.rst.j2' - exporter_attr_dynamic = AttrDynamicExporter() + exporter_attr_dynamic = AttrDynamicExporter(template_name='rst') output_attr_dynamic, _ = exporter_attr_dynamic.from_notebook_node(nb) assert "blah" in output_attr_dynamic exporter_attr_dynamic.raw_template = '' - assert exporter_attr_dynamic.template_file == "rst.tpl" + assert exporter_attr_dynamic.template_file == 'index.rst.j2' output_attr_dynamic, _ = exporter_attr_dynamic.from_notebook_node(nb) assert "blah" not in output_attr_dynamic @@ -229,7 +229,7 @@ def test_raw_template_constructor(self): nb = v4.new_notebook() nb.cells.append(v4.new_code_cell("some_text")) - output_constructor, _ = TemplateExporter( + output_constructor, _ = TemplateExporter(template_name='rst', raw_template=raw_template).from_notebook_node(nb) assert "blah" in output_constructor @@ -239,7 +239,7 @@ def test_raw_template_assignment(self): """ nb = v4.new_notebook() nb.cells.append(v4.new_code_cell("some_text")) - exporter_assign = TemplateExporter() + exporter_assign = TemplateExporter(template_name='rst') exporter_assign.raw_template = raw_template output_assign, _ = exporter_assign.from_notebook_node(nb) assert "blah" in output_assign @@ -250,7 +250,7 @@ def test_raw_template_reassignment(self): """ nb = v4.new_notebook() nb.cells.append(v4.new_code_cell("some_text")) - exporter_reassign = TemplateExporter() + exporter_reassign = TemplateExporter(template_name='rst') exporter_reassign.raw_template = raw_template output_reassign, _ = exporter_reassign.from_notebook_node(nb) assert "blah" in output_reassign @@ -270,7 +270,7 @@ def test_raw_template_deassignment(self): output_deassign, _ = exporter_deassign.from_notebook_node(nb) assert "blah" in output_deassign exporter_deassign.raw_template = '' - assert exporter_deassign.template_file == 'rst.tpl' + assert exporter_deassign.template_file == 'index.rst.j2' output_deassign, _ = exporter_deassign.from_notebook_node(nb) assert "blah" not in output_deassign @@ -289,7 +289,7 @@ def test_raw_template_dereassignment(self): output_dereassign, _ = exporter_dereassign.from_notebook_node(nb) assert "baz" in output_dereassign exporter_dereassign.raw_template = '' - assert exporter_dereassign.template_file == 'rst.tpl' + assert exporter_dereassign.template_file == 'index.rst.j2' output_dereassign, _ = exporter_dereassign.from_notebook_node(nb) assert "blah" not in output_dereassign @@ -317,8 +317,8 @@ def test_exclude_code_cell(self): } } c_no_io = Config(no_io) - exporter_no_io = TemplateExporter(config=c_no_io) - exporter_no_io.template_file = 'markdown' + exporter_no_io = TemplateExporter(config=c_no_io, template_name='markdown') + exporter_no_io.template_file = 'index.md.j2' nb_no_io, resources_no_io = exporter_no_io.from_filename(self._get_notebook()) assert not resources_no_io['global_content_filter']['include_input'] @@ -335,8 +335,8 @@ def test_exclude_code_cell(self): } } c_no_code = Config(no_code) - exporter_no_code = TemplateExporter(config=c_no_code) - exporter_no_code.template_file = 'markdown' + exporter_no_code = TemplateExporter(config=c_no_code, template_name='markdown') + exporter_no_code.template_file = 'index.md.j2' nb_no_code, resources_no_code = exporter_no_code.from_filename(self._get_notebook()) assert not resources_no_code['global_content_filter']['include_code'] @@ -375,8 +375,8 @@ def test_exclude_markdown(self): } c_no_md = Config(no_md) - exporter_no_md = TemplateExporter(config=c_no_md) - exporter_no_md.template_file = 'python' + exporter_no_md = TemplateExporter(config=c_no_md, template_name='python') + exporter_no_md.template_file = 'index.py.j2' nb_no_md, resources_no_md = exporter_no_md.from_filename(self._get_notebook()) assert not resources_no_md['global_content_filter']['include_markdown'] @@ -423,5 +423,6 @@ def _make_exporter(self, config=None): exporter = TemplateExporter(config=config) if not exporter.template_file: # give it a default if not specified - exporter.template_file = 'python' + exporter.template_name = 'python' + exporter.template_file = 'index.py.j2' return exporter diff --git a/nbconvert/nbconvertapp.py b/nbconvert/nbconvertapp.py index 0a6a7f865..1860552c5 100755 --- a/nbconvert/nbconvertapp.py +++ b/nbconvert/nbconvertapp.py @@ -51,7 +51,8 @@ def validate(self, obj, value): nbconvert_aliases.update(base_aliases) nbconvert_aliases.update({ 'to' : 'NbConvertApp.export_format', - 'template' : 'TemplateExporter.template_file', + 'template' : 'TemplateExporter.template_name', + 'template-file' : 'TemplateExporter.template_file', 'writer' : 'NbConvertApp.writer_class', 'post': 'NbConvertApp.postprocessor_class', 'output': 'NbConvertApp.output_base', @@ -191,7 +192,7 @@ def _classes_default(self): 'base', 'article' and 'report'. HTML includes 'basic' and 'full'. You can specify the flavor of the format used. - > jupyter nbconvert --to html --template basic mynotebook.ipynb + > jupyter nbconvert --to html --template lab mynotebook.ipynb You can also pipe the output to stdout, rather than a file diff --git a/nbconvert/preprocessors/csshtmlheader.py b/nbconvert/preprocessors/csshtmlheader.py index 191a2ea6c..0fa7b9bb8 100755 --- a/nbconvert/preprocessors/csshtmlheader.py +++ b/nbconvert/preprocessors/csshtmlheader.py @@ -9,9 +9,11 @@ import hashlib import nbconvert.resources -from traitlets import Unicode -from .base import Preprocessor +from traitlets import Unicode, Union, Type +from pygments.style import Style +from jupyterlab_pygments import JupyterStyle +from .base import Preprocessor try: from notebook import DEFAULT_STATIC_FILES_PATH @@ -28,8 +30,9 @@ class CSSHTMLHeaderPreprocessor(Preprocessor): help="CSS highlight class identifier" ).tag(config=True) - style = Unicode('default', - help='Name of the pygments style to use' + style = Union([Unicode('default'), Type(klass=Style)], + help='Name of the pygments style to use', + default_value=JupyterStyle ).tag(config=True) def __init__(self, *pargs, **kwargs): @@ -40,9 +43,9 @@ def preprocess(self, nb, resources): """Fetch and add CSS to the resource dictionary Fetch CSS from IPython and Pygments to add at the beginning - of the html files. Add this css in resources in the + of the html files. Add this css in resources in the "inlining.css" key - + Parameters ---------- nb : NotebookNode @@ -56,24 +59,13 @@ def preprocess(self, nb, resources): return nb, resources def _generate_header(self, resources): - """ - Fills self.header with lines of CSS extracted from IPython + """ + Fills self.header with lines of CSS extracted from IPython and Pygments. """ from pygments.formatters import HtmlFormatter header = [] - - # Construct path to Jupyter CSS - sheet_filename = os.path.join( - os.path.dirname(nbconvert.resources.__file__), - 'style.min.css', - ) - - # Load style CSS file. - with io.open(sheet_filename, encoding='utf-8') as f: - header.append(f.read()) - - # Add pygments CSS + formatter = HtmlFormatter(style=self.style) pygments_css = formatter.get_style_defs(self.highlight_class) header.append(pygments_css) diff --git a/nbconvert/templates/skeleton/null.tpl b/nbconvert/templates/skeleton/null.tpl deleted file mode 100644 index 32886a867..000000000 --- a/nbconvert/templates/skeleton/null.tpl +++ /dev/null @@ -1,102 +0,0 @@ -{# - -DO NOT USE THIS AS A BASE, -IF YOU ARE COPY AND PASTING THIS FILE -YOU ARE PROBABLY DOING THINGS INCORRECTLY. - -Null template, does nothing except defining a basic structure -To layout the different blocks of a notebook. - -Subtemplates can override blocks to define their custom representation. - -If one of the block you do overwrite is not a leave block, consider -calling super. - -{%- block nonLeaveBlock -%} - #add stuff at beginning - {{ super() }} - #add stuff at end -{%- endblock nonLeaveBlock -%} - -consider calling super even if it is a leave block, we might insert more blocks later. - -#} -{%- block header -%} -{%- endblock header -%} -{%- block body -%} -{%- for cell in nb.cells -%} - {%- block any_cell scoped -%} - {%- if cell.cell_type == 'code'-%} - {%- if resources.global_content_filter.include_code -%} - {%- block codecell scoped -%} - {%- if resources.global_content_filter.include_input and not cell.get("transient",{}).get("remove_source", false) -%} - {%- block input_group -%} - {%- if resources.global_content_filter.include_input_prompt -%} - {%- block in_prompt -%}{%- endblock in_prompt -%} - {%- endif -%} - {%- block input -%}{%- endblock input -%} - {%- endblock input_group -%} - {%- endif -%} - {%- if cell.outputs and resources.global_content_filter.include_output -%} - {%- block output_group -%} - {%- if resources.global_content_filter.include_output_prompt -%} - {%- block output_prompt -%}{%- endblock output_prompt -%} - {%- endif -%} - {%- block outputs scoped -%} - {%- for output in cell.outputs -%} - {%- block output scoped -%} - {%- if output.output_type == 'execute_result' -%} - {%- block execute_result scoped -%}{%- endblock execute_result -%} - {%- elif output.output_type == 'stream' -%} - {%- block stream scoped -%} - {%- if output.name == 'stdout' -%} - {%- block stream_stdout scoped -%} - {%- endblock stream_stdout -%} - {%- elif output.name == 'stderr' -%} - {%- block stream_stderr scoped -%} - {%- endblock stream_stderr -%} - {%- endif -%} - {%- endblock stream -%} - {%- elif output.output_type == 'display_data' -%} - {%- block display_data scoped -%} - {%- block data_priority scoped -%} - {%- endblock data_priority -%} - {%- endblock display_data -%} - {%- elif output.output_type == 'error' -%} - {%- block error scoped -%} - {%- for line in output.traceback -%} - {%- block traceback_line scoped -%}{%- endblock traceback_line -%} - {%- endfor -%} - {%- endblock error -%} - {%- endif -%} - {%- endblock output -%} - {%- endfor -%} - {%- endblock outputs -%} - {%- endblock output_group -%} - {%- endif -%} - {%- endblock codecell -%} - {%- endif -%} - {%- elif cell.cell_type in ['markdown'] -%} - {%- if resources.global_content_filter.include_markdown and not cell.get("transient",{}).get("remove_source", false) -%} - {%- block markdowncell scoped-%} {%- endblock markdowncell -%} - {%- endif -%} - {%- elif cell.cell_type in ['raw'] -%} - {%- if resources.global_content_filter.include_raw and not cell.get("transient",{}).get("remove_source", false) -%} - {%- block rawcell scoped -%} - {%- if cell.metadata.get('raw_mimetype', '').lower() in resources.get('raw_mimetypes', ['']) -%} - {{ cell.source }} - {%- endif -%} - {%- endblock rawcell -%} - {%- endif -%} - {%- else -%} - {%- if resources.global_content_filter.include_unknown and not cell.get("transient",{}).get("remove_source", false) -%} - {%- block unknowncell scoped-%} - {%- endblock unknowncell -%} - {%- endif -%} - {%- endif -%} - {%- endblock any_cell -%} -{%- endfor -%} -{%- endblock body -%} - -{%- block footer -%} -{%- endblock footer -%} diff --git a/nbconvert/tests/fake_exporters.py b/nbconvert/tests/fake_exporters.py index bf75cca51..f3ec356b3 100644 --- a/nbconvert/tests/fake_exporters.py +++ b/nbconvert/tests/fake_exporters.py @@ -19,3 +19,7 @@ def _file_extension_default(self): The new file extension is `.test_ext` """ return '.test_ext' + + @default('template_extension') + def _template_extension_default(self): + return '.html.j2' diff --git a/nbconvert/tests/test_nbconvertapp.py b/nbconvert/tests/test_nbconvertapp.py index 48b876b59..b6b5590f0 100644 --- a/nbconvert/tests/test_nbconvertapp.py +++ b/nbconvert/tests/test_nbconvertapp.py @@ -91,27 +91,27 @@ def test_explicit(self): assert os.path.isfile('notebook2.py') def test_absolute_template_file(self): - """--template '/path/to/template.tpl'""" + """--template-file '/path/to/template.tpl'""" with self.create_temp_cwd(['notebook*.ipynb']), tempdir.TemporaryDirectory() as td: template = os.path.join(td, 'mytemplate.tpl') test_output = 'success!' with open(template, 'w') as f: f.write(test_output) - self.nbconvert('--log-level 0 notebook2 --template %s' % template) + self.nbconvert('--log-level 0 notebook2 --template-file %s' % template) assert os.path.isfile('notebook2.html') with open('notebook2.html') as f: text = f.read() assert text == test_output def test_relative_template_file(self): - """Test --template 'relative/path.tpl'""" + """Test --template-file 'relative/path.tpl'""" with self.create_temp_cwd(['notebook*.ipynb']): os.mkdir('relative') template = os.path.join('relative', 'path.tpl') test_output = 'success!' with open(template, 'w') as f: f.write(test_output) - self.nbconvert('--log-level 0 notebook2 --template %s' % template) + self.nbconvert('--log-level 0 notebook2 --template-file %s' % template) assert os.path.isfile('notebook2.html') with open('notebook2.html') as f: text = f.read() @@ -171,7 +171,7 @@ def test_png_base64_html_ok(self): """Is embedded png data well formed in HTML?""" with self.create_temp_cwd(['notebook2.ipynb']): self.nbconvert('--log-level 0 --to HTML ' - 'notebook2.ipynb --template full') + 'notebook2.ipynb --template lab') assert os.path.isfile('notebook2.html') with open('notebook2.html') as f: assert "data:image/png;base64,b'" not in f.read() diff --git a/setup.py b/setup.py index 1cf78b1bb..2dcf91a1a 100644 --- a/setup.py +++ b/setup.py @@ -65,13 +65,27 @@ ], } - notebook_css_version = '5.4.0' -css_url = "https://cdn.jupyter.org/notebook/%s/style/style.min.css" % notebook_css_version +notebook_css_url = "https://cdn.jupyter.org/notebook/%s/style/style.min.css" % notebook_css_version + + +jupyterlab_css_version = '0.1.0' +jupyterlab_css_url = "https://unpkg.com/@jupyterlab/nbconvert-css@%s/style/index.css" % jupyterlab_css_version + +jupyterlab_theme_light_version = '0.19.1' +jupyterlab_theme_light_url = "https://unpkg.com/@jupyterlab/theme-light-extension@%s/static/embed.css" % jupyterlab_theme_light_version + +jupyterlab_theme_dark_version = '0.19.1' +jupyterlab_theme_dark_url = "https://unpkg.com/@jupyterlab/theme-dark-extension@%s/static/embed.css" % jupyterlab_theme_dark_version + +template_css_urls = { + 'lab': [(jupyterlab_css_url, 'index.css'), (jupyterlab_theme_light_url, 'theme-light.css'), (jupyterlab_theme_dark_url, 'theme-dark.css')], + 'classic': [(notebook_css_url, 'style.css')] +} class FetchCSS(Command): - description = "Fetch Notebook CSS from Jupyter CDN" + description = "Fetch CSS from CDN" user_options = [] def initialize_options(self): pass @@ -79,9 +93,9 @@ def initialize_options(self): def finalize_options(self): pass - def _download(self): + def _download(self, url): try: - return urlopen(css_url).read() + return urlopen(url).read() except Exception as e: if 'ssl' in str(e).lower(): try: @@ -91,39 +105,48 @@ def _download(self): raise e else: print("Failed, trying again with PycURL to avoid outdated SSL.", file=sys.stderr) - return self._download_pycurl() + return self._download_pycurl(url) raise e - def _download_pycurl(self): + def _download_pycurl(self, url): """Download CSS with pycurl, in case of old SSL (e.g. Python < 2.7.9).""" import pycurl c = pycurl.Curl() - c.setopt(c.URL, css_url) + c.setopt(c.URL, url) buf = BytesIO() c.setopt(c.WRITEDATA, buf) c.perform() return buf.getvalue() def run(self): - dest = os.path.join('nbconvert', 'resources', 'style.min.css') - if not os.path.exists('.git') and os.path.exists(dest): - # not running from git, nothing to do - return - print("Downloading CSS: %s" % css_url) - try: - css = self._download() - except Exception as e: - msg = "Failed to download css from %s: %s" % (css_url, e) - print(msg, file=sys.stderr) - if os.path.exists(dest): - print("Already have CSS: %s, moving on." % dest) - else: - raise OSError("Need Notebook CSS to proceed: %s" % dest) - return - - with open(dest, 'wb') as f: - f.write(css) - print("Downloaded Notebook CSS to %s" % dest) + for template_name, resources in template_css_urls.items(): + for url, filename in resources: + directory = os.path.join('share', 'jupyter', 'nbconvert', 'templates', template_name, 'static') + dest = os.path.join(directory, filename) + if not os.path.exists(directory): + os.makedirs(directory) + if not os.path.exists('.git') and os.path.exists(dest): + # not running from git, nothing to do + return + print("Downloading CSS: %s" % url) + try: + css = self._download(url) + except Exception as e: + msg = "Failed to download css from %s: %s" % (url, e) + print(msg, file=sys.stderr) + if os.path.exists(dest): + print("Already have CSS: %s, moving on." % dest) + else: + raise OSError("Need CSS to proceed.") + return + + with open(dest, 'wb') as f: + f.write(css) + print("Downloaded Notebook CSS to %s" % dest) + + # update package data in case this created new files + self.distribution.data_files = get_data_files() + update_package_data(self.distribution) cmdclass = {'css': FetchCSS} @@ -160,6 +183,24 @@ def run(self): with io.open(pjoin(here, 'README.md'), encoding='utf-8') as f: long_description = f.read() + +def update_package_data(distribution): + """update package_data to catch changes during setup""" + build_py = distribution.get_command_obj('build_py') + # distribution.package_data = find_package_data() + # re-init build_py options which load package_data + build_py.finalize_options() + + +def get_data_files(): + # Add all the templates + data_files = [] + for (dirpath, dirnames, filenames) in os.walk('share/jupyter/nbconvert/templates/'): + if filenames: + data_files.append((dirpath, [os.path.join(dirpath, filename) for filename in filenames])) + return data_files + + setup_args = dict( name = name, description = "Converting Jupyter Notebooks", @@ -168,6 +209,7 @@ def run(self): packages = packages, long_description= long_description, package_data = package_data, + data_files = get_data_files(), cmdclass = cmdclass, python_requires = '>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*', author = 'Jupyter Development Team', @@ -199,7 +241,8 @@ def run(self): setup_args['install_requires'] = [ 'mistune>=0.8.1,<2', 'jinja2>=2.4', - 'pygments', + 'pygments>=2.4.1', + 'jupyterlab_pygments', 'traitlets>=4.2', 'jupyter_core', 'nbformat>=4.4', diff --git a/share/jupyter/nbconvert/templates/asciidoc/conf.json b/share/jupyter/nbconvert/templates/asciidoc/conf.json new file mode 100644 index 000000000..37cdfa7bc --- /dev/null +++ b/share/jupyter/nbconvert/templates/asciidoc/conf.json @@ -0,0 +1,6 @@ +{ + "base_template": "base", + "mimetypes": { + "text/asciidoc": true + } +} \ No newline at end of file diff --git a/nbconvert/templates/asciidoc.tpl b/share/jupyter/nbconvert/templates/asciidoc/index.asciidoc.j2 similarity index 98% rename from nbconvert/templates/asciidoc.tpl rename to share/jupyter/nbconvert/templates/asciidoc/index.asciidoc.j2 index 8f32c4a6e..1512a2daa 100644 --- a/nbconvert/templates/asciidoc.tpl +++ b/share/jupyter/nbconvert/templates/asciidoc/index.asciidoc.j2 @@ -1,4 +1,4 @@ -{% extends 'display_priority.tpl' %} +{% extends 'display_priority.j2' %} {% block input %} diff --git a/nbconvert/templates/html/basic.tpl b/share/jupyter/nbconvert/templates/base/base.html.j2 similarity index 98% rename from nbconvert/templates/html/basic.tpl rename to share/jupyter/nbconvert/templates/base/base.html.j2 index 110e86423..e9bd3eb54 100644 --- a/nbconvert/templates/html/basic.tpl +++ b/share/jupyter/nbconvert/templates/base/base.html.j2 @@ -1,5 +1,5 @@ -{%- extends 'display_priority.tpl' -%} -{% from 'celltags.tpl' import celltags %} +{%- extends 'display_priority.j2' -%} +{% from 'celltags.j2' import celltags %} {% block codecell %}

@@ -240,7 +240,7 @@ var element = $('#{{ div_id }}');
- +{%- endblock html_head_js_requirejs -%} +{%- endblock html_head_js -%} {% block ipywidgets %} {%- if "widgets" in nb.metadata -%} @@ -43,12 +48,18 @@ {%- endif -%} {% endblock ipywidgets %} +{% block extra_css %} +{% endblock extra_css %} + {% for css in resources.inlining.css -%} {% endfor %} + +{% block notebook_css %} +{{ resources.include_css("static/style.css") }} +{% endblock notebook_css %} + +{% block custom_css %} +{% endblock custom_css %} {{ mathjax() }} + + +{%- block html_head_css -%} +{%- endblock html_head_css -%} + {%- endblock html_head -%} {%- endblock header -%} -{% block body %} +{# using the body block like this makes is difficult to be composable, added body_content/body_cells block for that#} +{% block body_header %}
-{{ super() }} +{% endblock body_header %} + +{% block body_footer %}
-{%- endblock body %} +{% endblock body_footer %} {% block footer %} +{% block footer_js %} +{% endblock footer_js %} {{ super() }} {% endblock footer %} diff --git a/share/jupyter/nbconvert/templates/lab/base.html.j2 b/share/jupyter/nbconvert/templates/lab/base.html.j2 new file mode 100644 index 000000000..eb9c45feb --- /dev/null +++ b/share/jupyter/nbconvert/templates/lab/base.html.j2 @@ -0,0 +1,270 @@ +{%- extends 'display_priority.j2' -%} +{% from 'celltags.j2' import celltags %} + +{% block codecell %} +{%- if not cell.outputs -%} +{%- set extra_class="jp-mod-noOutput" -%} +{%- endif -%} + +{%- endblock codecell %} + +{% block input_group -%} + +{% endblock input_group %} + +{% block input %} + +{%- endblock input %} + +{% block output_group %} + +{% endblock output_group %} + +{% block outputs %} + +{% endblock outputs %} + +{% block in_prompt -%} + +{%- endblock in_prompt %} + +{% block empty_in_prompt -%} + +{%- endblock empty_in_prompt %} + +{# + output_prompt doesn't do anything in HTML, + because there is a prompt div in each output area (see output block) + #} +{% block output_prompt %} +{% endblock output_prompt %} + +{% block output_area_prompt %} + +{% endblock output_area_prompt %} + +{% block output %} + +{% endblock output %} + +{% block markdowncell scoped %} + +{%- endblock markdowncell %} + +{% block unknowncell scoped %} +unknown type {{ cell.type }} +{% endblock unknowncell %} + +{% block execute_result -%} +{%- set extra_class="jp-OutputArea-executeResult" -%} +{% block data_priority scoped %} +{{ super() }} +{% endblock data_priority %} +{%- set extra_class="" -%} +{%- endblock execute_result %} + +{% block stream_stdout -%} + +{%- endblock stream_stdout %} + +{% block stream_stderr -%} + +{%- endblock stream_stderr %} + +{% block data_svg scoped -%} + +{%- endblock data_svg %} + +{% block data_html scoped -%} + +{%- endblock data_html %} + +{% block data_markdown scoped -%} + +{%- endblock data_markdown %} + +{% block data_png scoped %} + +{%- endblock data_png %} + +{% block data_jpg scoped %} + +{%- endblock data_jpg %} + +{% block data_latex scoped %} + +{%- endblock data_latex %} + +{% block error -%} + +{%- endblock error %} + +{%- block traceback_line %} +{{ line | ansi2html }} +{%- endblock traceback_line %} + +{%- block data_text scoped %} + +{%- endblock -%} + +{# + ############################################################################### + # TODO: how to better handle JavaScript repr? # + ############################################################################### + #} + +{% set div_id = uuid4() %} +{%- block data_javascript scoped %} +
+ +{%- endblock -%} + +{%- block data_widget_state scoped %} +{% set div_id = uuid4() %} +{% set datatype_list = output.data | filter_data_type %} +{% set datatype = datatype_list[0]%} +
+
+ + +
+{%- endblock data_widget_state -%} + +{%- block data_widget_view scoped %} +{% set div_id = uuid4() %} +{% set datatype_list = output.data | filter_data_type %} +{% set datatype = datatype_list[0]%} +
+ +{%- endblock data_widget_view -%} + +{%- block footer %} +{% set mimetype = 'application/vnd.jupyter.widget-state+json'%} +{% if mimetype in nb.metadata.get("widgets",{})%} + +{% endif %} +{{ super() }} +{%- endblock footer-%} diff --git a/share/jupyter/nbconvert/templates/lab/conf.json b/share/jupyter/nbconvert/templates/lab/conf.json new file mode 100644 index 000000000..e11030fc9 --- /dev/null +++ b/share/jupyter/nbconvert/templates/lab/conf.json @@ -0,0 +1,6 @@ +{ + "base_template": "classic", + "mimetypes": { + "text/html": true + } +} \ No newline at end of file diff --git a/share/jupyter/nbconvert/templates/lab/index.html.j2 b/share/jupyter/nbconvert/templates/lab/index.html.j2 new file mode 100644 index 000000000..e1c7bec00 --- /dev/null +++ b/share/jupyter/nbconvert/templates/lab/index.html.j2 @@ -0,0 +1,29 @@ +{%- extends 'classic/index.html.j2' -%} + + +{% block notebook_css %} +{{ resources.include_css("static/index.css") }} +{% if resources.theme == 'dark' %} + {{ resources.include_css("static/theme-dark.css") }} +{% else %} + {{ resources.include_css("static/theme-light.css") }} +{% endif %} + + +{% endblock notebook_css %} + + +{%- block body_header -%} +{% if resources.theme == 'dark' %} + +{% else %} + +{% endif %} +{%- endblock body_header -%} diff --git a/nbconvert/templates/latex/base.tplx b/share/jupyter/nbconvert/templates/latex/base.tex.j2 similarity index 99% rename from nbconvert/templates/latex/base.tplx rename to share/jupyter/nbconvert/templates/latex/base.tex.j2 index f3335be23..68f763f30 100644 --- a/nbconvert/templates/latex/base.tplx +++ b/share/jupyter/nbconvert/templates/latex/base.tex.j2 @@ -4,7 +4,7 @@ functions. Figures, data_text, This template defines defines a default docclass, the inheriting class should override this.-=)) -((*- extends 'document_contents.tplx' -*)) +((*- extends 'document_contents.tex.j2' -*)) %=============================================================================== % Abstract overrides diff --git a/share/jupyter/nbconvert/templates/latex/conf.json b/share/jupyter/nbconvert/templates/latex/conf.json new file mode 100644 index 000000000..1ac7a640d --- /dev/null +++ b/share/jupyter/nbconvert/templates/latex/conf.json @@ -0,0 +1,7 @@ +{ + "base_template": "base", + "mimetypes": { + "text/latex": true, + "application/pdf": true + } +} diff --git a/nbconvert/templates/latex/skeleton/display_priority.tplx b/share/jupyter/nbconvert/templates/latex/display_priority.j2 similarity index 98% rename from nbconvert/templates/latex/skeleton/display_priority.tplx rename to share/jupyter/nbconvert/templates/latex/display_priority.j2 index 3b9f7001d..9f33a74e1 100644 --- a/nbconvert/templates/latex/skeleton/display_priority.tplx +++ b/share/jupyter/nbconvert/templates/latex/display_priority.j2 @@ -2,7 +2,7 @@ To edit this file, please refer to ../../skeleton/README.md =)) -((*- extends 'null.tplx' -*)) +((*- extends 'null.j2' -*)) ((=display data priority=)) diff --git a/nbconvert/templates/latex/document_contents.tplx b/share/jupyter/nbconvert/templates/latex/document_contents.tex.j2 similarity index 98% rename from nbconvert/templates/latex/document_contents.tplx rename to share/jupyter/nbconvert/templates/latex/document_contents.tex.j2 index 24f969596..5e0fa7881 100644 --- a/nbconvert/templates/latex/document_contents.tplx +++ b/share/jupyter/nbconvert/templates/latex/document_contents.tex.j2 @@ -1,4 +1,4 @@ -((*- extends 'display_priority.tplx' -*)) +((*- extends 'display_priority.j2' -*)) %=============================================================================== % Support blocks diff --git a/nbconvert/templates/latex/article.tplx b/share/jupyter/nbconvert/templates/latex/index.tex.j2 similarity index 89% rename from nbconvert/templates/latex/article.tplx rename to share/jupyter/nbconvert/templates/latex/index.tex.j2 index b87a7ff18..840c9d4de 100644 --- a/nbconvert/templates/latex/article.tplx +++ b/share/jupyter/nbconvert/templates/latex/index.tex.j2 @@ -1,7 +1,7 @@ ((=- Default to the notebook output style -=)) ((*- if not cell_style is defined -*)) - ((* set cell_style = 'style_jupyter.tplx' *)) + ((* set cell_style = 'style_jupyter.tex.j2' *)) ((*- endif -*)) ((=- Inherit from the specified cell style. -=)) diff --git a/nbconvert/templates/latex/skeleton/null.tplx b/share/jupyter/nbconvert/templates/latex/null.j2 similarity index 100% rename from nbconvert/templates/latex/skeleton/null.tplx rename to share/jupyter/nbconvert/templates/latex/null.j2 diff --git a/nbconvert/templates/latex/report.tplx b/share/jupyter/nbconvert/templates/latex/report.tex.j2 similarity index 94% rename from nbconvert/templates/latex/report.tplx rename to share/jupyter/nbconvert/templates/latex/report.tex.j2 index 1ade514c5..669562a2f 100644 --- a/nbconvert/templates/latex/report.tplx +++ b/share/jupyter/nbconvert/templates/latex/report.tex.j2 @@ -1,7 +1,7 @@ % Default to the notebook output style ((* if not cell_style is defined *)) - ((* set cell_style = 'style_ipython.tplx' *)) + ((* set cell_style = 'style_ipython.tex.j2' *)) ((* endif *)) % Inherit from the specified cell style. diff --git a/nbconvert/templates/latex/style_bw_ipython.tplx b/share/jupyter/nbconvert/templates/latex/style_bw_ipython.tex.j2 similarity index 98% rename from nbconvert/templates/latex/style_bw_ipython.tplx rename to share/jupyter/nbconvert/templates/latex/style_bw_ipython.tex.j2 index 43ccc5563..07fdfa391 100644 --- a/nbconvert/templates/latex/style_bw_ipython.tplx +++ b/share/jupyter/nbconvert/templates/latex/style_bw_ipython.tex.j2 @@ -1,6 +1,6 @@ ((= Black&white ipython input/output style =)) -((*- extends 'base.tplx' -*)) +((*- extends 'base.tex.j2' -*)) %=============================================================================== % Input diff --git a/nbconvert/templates/latex/style_bw_python.tplx b/share/jupyter/nbconvert/templates/latex/style_bw_python.tex.j2 similarity index 93% rename from nbconvert/templates/latex/style_bw_python.tplx rename to share/jupyter/nbconvert/templates/latex/style_bw_python.tex.j2 index d309ca424..dea279405 100644 --- a/nbconvert/templates/latex/style_bw_python.tplx +++ b/share/jupyter/nbconvert/templates/latex/style_bw_python.tex.j2 @@ -1,6 +1,6 @@ ((= Black&white Python input/output style =)) -((*- extends 'base.tplx' -*)) +((*- extends 'base.tex.j2' -*)) %=============================================================================== % Input diff --git a/nbconvert/templates/latex/style_ipython.tplx b/share/jupyter/nbconvert/templates/latex/style_ipython.tex.j2 similarity index 96% rename from nbconvert/templates/latex/style_ipython.tplx rename to share/jupyter/nbconvert/templates/latex/style_ipython.tex.j2 index 10f01a80b..9fa238baf 100644 --- a/nbconvert/templates/latex/style_ipython.tplx +++ b/share/jupyter/nbconvert/templates/latex/style_ipython.tex.j2 @@ -1,6 +1,6 @@ -((= IPython input/output style =)) + style_bw_python.\TeX((= IPython input/output style =)) -((*- extends 'base.tplx' -*)) +((*- extends 'base.tex.j2' -*)) % Custom definitions ((* block definitions *)) diff --git a/nbconvert/templates/latex/style_jupyter.tplx b/share/jupyter/nbconvert/templates/latex/style_jupyter.tex.j2 similarity index 99% rename from nbconvert/templates/latex/style_jupyter.tplx rename to share/jupyter/nbconvert/templates/latex/style_jupyter.tex.j2 index 66699287c..7ecefb5dc 100644 --- a/nbconvert/templates/latex/style_jupyter.tplx +++ b/share/jupyter/nbconvert/templates/latex/style_jupyter.tex.j2 @@ -1,5 +1,5 @@ ((=- IPython input/output style -=)) -((*- extends 'base.tplx' -*)) +((*- extends 'base.tex.j2' -*)) ((*- block packages -*)) \usepackage[breakable]{tcolorbox} diff --git a/nbconvert/templates/latex/style_python.tplx b/share/jupyter/nbconvert/templates/latex/style_python.tex.j2 similarity index 95% rename from nbconvert/templates/latex/style_python.tplx rename to share/jupyter/nbconvert/templates/latex/style_python.tex.j2 index c21fc5a6b..60cc2ad71 100644 --- a/nbconvert/templates/latex/style_python.tplx +++ b/share/jupyter/nbconvert/templates/latex/style_python.tex.j2 @@ -1,6 +1,6 @@ ((= Python input/output style =)) -((*- extends 'base.tplx' -*)) +((*- extends 'base.tex.j2' -*)) % Custom definitions ((* block definitions *)) diff --git a/share/jupyter/nbconvert/templates/markdown/conf.json b/share/jupyter/nbconvert/templates/markdown/conf.json new file mode 100644 index 000000000..98b819dd9 --- /dev/null +++ b/share/jupyter/nbconvert/templates/markdown/conf.json @@ -0,0 +1,6 @@ +{ + "base_template": "base", + "mimetypes": { + "text/markdown": true + } +} \ No newline at end of file diff --git a/nbconvert/templates/markdown.tpl b/share/jupyter/nbconvert/templates/markdown/index.md.j2 similarity index 97% rename from nbconvert/templates/markdown.tpl rename to share/jupyter/nbconvert/templates/markdown/index.md.j2 index ce694d710..8906eff87 100644 --- a/nbconvert/templates/markdown.tpl +++ b/share/jupyter/nbconvert/templates/markdown/index.md.j2 @@ -1,4 +1,4 @@ -{% extends 'display_priority.tpl' %} +{% extends 'display_priority.j2' %} {% block in_prompt %} diff --git a/share/jupyter/nbconvert/templates/python/conf.json b/share/jupyter/nbconvert/templates/python/conf.json new file mode 100644 index 000000000..d7ccd0358 --- /dev/null +++ b/share/jupyter/nbconvert/templates/python/conf.json @@ -0,0 +1,6 @@ +{ + "base_template": "base", + "mimetypes": { + "text/x-python": true + } +} \ No newline at end of file diff --git a/nbconvert/templates/python.tpl b/share/jupyter/nbconvert/templates/python/index.py.j2 similarity index 94% rename from nbconvert/templates/python.tpl rename to share/jupyter/nbconvert/templates/python/index.py.j2 index eed2f923f..a09fe9d1f 100644 --- a/nbconvert/templates/python.tpl +++ b/share/jupyter/nbconvert/templates/python/index.py.j2 @@ -1,4 +1,4 @@ -{%- extends 'null.tpl' -%} +{%- extends 'null.j2' -%} {%- block header -%} #!/usr/bin/env python diff --git a/share/jupyter/nbconvert/templates/reveal/conf.json b/share/jupyter/nbconvert/templates/reveal/conf.json new file mode 100644 index 000000000..e11030fc9 --- /dev/null +++ b/share/jupyter/nbconvert/templates/reveal/conf.json @@ -0,0 +1,6 @@ +{ + "base_template": "classic", + "mimetypes": { + "text/html": true + } +} \ No newline at end of file diff --git a/nbconvert/templates/html/slides_reveal.tpl b/share/jupyter/nbconvert/templates/reveal/index.html.j2 similarity index 98% rename from nbconvert/templates/html/slides_reveal.tpl rename to share/jupyter/nbconvert/templates/reveal/index.html.j2 index 0ddf84473..309d88c37 100644 --- a/nbconvert/templates/html/slides_reveal.tpl +++ b/share/jupyter/nbconvert/templates/reveal/index.html.j2 @@ -1,5 +1,5 @@ -{%- extends 'basic.tpl' -%} -{% from 'mathjax.tpl' import mathjax %} +{%- extends 'classic/index.html.j2' -%} +{% from 'mathjax.html.j2' import mathjax %} {%- block any_cell scoped -%} {%- if cell.metadata.get('slide_start', False) -%} diff --git a/share/jupyter/nbconvert/templates/rst/conf.json b/share/jupyter/nbconvert/templates/rst/conf.json new file mode 100644 index 000000000..93bcd13ae --- /dev/null +++ b/share/jupyter/nbconvert/templates/rst/conf.json @@ -0,0 +1,6 @@ +{ + "base_template": "base", + "mimetypes": { + "text/restructuredtext": true + } +} \ No newline at end of file diff --git a/nbconvert/templates/rst.tpl b/share/jupyter/nbconvert/templates/rst/index.rst.j2 similarity index 98% rename from nbconvert/templates/rst.tpl rename to share/jupyter/nbconvert/templates/rst/index.rst.j2 index bf070c1b9..bda5c5fb9 100644 --- a/nbconvert/templates/rst.tpl +++ b/share/jupyter/nbconvert/templates/rst/index.rst.j2 @@ -1,4 +1,4 @@ -{%- extends 'display_priority.tpl' -%} +{%- extends 'display_priority.j2' -%} {% block in_prompt %} diff --git a/share/jupyter/nbconvert/templates/script/conf.json b/share/jupyter/nbconvert/templates/script/conf.json new file mode 100644 index 000000000..f2793d779 --- /dev/null +++ b/share/jupyter/nbconvert/templates/script/conf.json @@ -0,0 +1,6 @@ +{ + "base_template": "base", + "mimetypes": { + "text/plain": true + } +} \ No newline at end of file diff --git a/nbconvert/templates/script.tpl b/share/jupyter/nbconvert/templates/script/script.j2 similarity index 68% rename from nbconvert/templates/script.tpl rename to share/jupyter/nbconvert/templates/script/script.j2 index cbd971d06..3b30ea89e 100644 --- a/nbconvert/templates/script.tpl +++ b/share/jupyter/nbconvert/templates/script/script.j2 @@ -1,4 +1,4 @@ -{%- extends 'null.tpl' -%} +{%- extends 'null.j2' -%} {% block input %} {{ cell.source }} From ae74de3a45b157996df548ac6c6666f3f1082a68 Mon Sep 17 00:00:00 2001 From: Ryan Beesley Date: Wed, 16 Oct 2019 00:03:17 -0700 Subject: [PATCH 399/671] Update svg2pdf.py to search the PATH for inkscape Instead of relying on the registry to locate inkscape, for Python 3.3+, use the shutil.which function to locate the file. If this fails because inkscape is not in the path or if the version of Python is less than 3.3, then fall back to the existing methods. This has the advantage of being a platform independent process as shutil.which is designed to be platform agnostic. This PR fixes the problems seen in issue #139 and #456 (as well as probably other issues), for resolving the path on Windows. This doesn't invalidate the work in PR #1008, which may help with the registry method, but I believe this change eliminates the need for PR #1008 for most by utilizing a more standard approach. Where the registry method is better is when inkscape is only in the registry and not in the PATH, but finding the file in the PATH is bound to be more resilient to change such as might happen if files on the disk are moved and yet the registry key was not updated with the change, or the registry key was left over and not removed from an old install. This is intended to compliment the existing method rather than replace. --- nbconvert/preprocessors/svg2pdf.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/nbconvert/preprocessors/svg2pdf.py b/nbconvert/preprocessors/svg2pdf.py index 60b23b8b7..aff14d9fa 100644 --- a/nbconvert/preprocessors/svg2pdf.py +++ b/nbconvert/preprocessors/svg2pdf.py @@ -17,6 +17,12 @@ from .convertfigures import ConvertFiguresPreprocessor +if sys.version_info >= (3,3): + from shutil import which + get_inkscape_path = which('inkscape') +else: + get_inkscape_path = None + INKSCAPE_APP = '/Applications/Inkscape.app/Contents/Resources/bin/inkscape' @@ -58,6 +64,8 @@ def _command_default(self): inkscape = Unicode(help="The path to Inkscape, if necessary").tag(config=True) @default('inkscape') def _inkscape_default(self): + if get_inkscape_path is not None: + return get_inkscape_path if sys.platform == "darwin": if os.path.isfile(INKSCAPE_APP): return INKSCAPE_APP From aa68ea352d26f195c7d106935f695ce4d8a8b6dd Mon Sep 17 00:00:00 2001 From: Matthew Seal Date: Wed, 16 Oct 2019 12:58:13 -0700 Subject: [PATCH 400/671] Added circleCI Badge for master branch --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 150266e85..363e2b332 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,7 @@ [![Documentation Status](https://readthedocs.org/projects/nbconvert/badge/?version=latest)](https://nbconvert.readthedocs.io/en/latest/?badge=latest) [![Documentation Status](https://readthedocs.org/projects/nbconvert/badge/?version=stable)](http://nbconvert.readthedocs.io/en/stable/?badge=stable) [![codecov.io](https://codecov.io/github/jupyter/nbconvert/coverage.svg?branch=master)](https://codecov.io/github/jupyter/nbconvert?branch=master) +[![CircleCI Docs Status](https://circleci.com/gh/jupyter/nbconvert/tree/master.svg?style=svg)](https://circleci.com/gh/jupyter/nbconvert/tree/master) The **nbconvert** tool, `jupyter nbconvert`, converts notebooks to various other From 945c616121709f5b718414f7391fb5eea6414401 Mon Sep 17 00:00:00 2001 From: Matthew Seal Date: Wed, 16 Oct 2019 20:54:06 -0700 Subject: [PATCH 401/671] Marking occasionally failing parallel test in python 2 as python 3 only --- nbconvert/preprocessors/tests/test_execute.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/nbconvert/preprocessors/tests/test_execute.py b/nbconvert/preprocessors/tests/test_execute.py index ce7a95f46..243a725da 100644 --- a/nbconvert/preprocessors/tests/test_execute.py +++ b/nbconvert/preprocessors/tests/test_execute.py @@ -266,7 +266,8 @@ def test_run_all_notebooks(input_name, opts): input_nb, output_nb = run_notebook(input_file, opts, notebook_resources()) assert_notebooks_equal(input_nb, output_nb) - +@pytest.mark.skipif(not PY3, + reason = "Not tested for Python 2") def test_parallel_notebooks(capfd, tmpdir): """Two notebooks should be able to be run simultaneously without problems. From fb52e7d6787e2b3bbb9a340b2afa7603b78c5ddf Mon Sep 17 00:00:00 2001 From: Matthias Geier Date: Sat, 19 Oct 2019 22:59:54 +0200 Subject: [PATCH 402/671] CircleCI: Use conda root environment, no need to "activate" --- .circleci/config.yml | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 852d6c8fc..3d4699330 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -12,8 +12,8 @@ jobs: working_directory: ~/checkout environment: + BASH_ENV: /root/.bashrc MINICONDA_INSTALLER: https://repo.continuum.io/miniconda/Miniconda3-latest-Linux-x86_64.sh - ACTIVATE: source /root/miniconda/bin/activate nbconvert_docs PIP_INSTALL: python3 -m pip install --user --progress-bar off --upgrade steps: @@ -35,11 +35,12 @@ jobs: else echo Miniconda is already installed fi + /root/miniconda/bin/conda init bash - run: name: Creating/Updating conda environment command: | - /root/miniconda/bin/conda env update -f docs/environment.yml --prune + conda env update -f docs/environment.yml -n root --prune - save_cache: key: v1-miniconda-{{ .Branch }}-{{ checksum "docs/environment.yml" }} @@ -49,13 +50,11 @@ jobs: - run: name: Installing nbconvert command: | - $ACTIVATE $PIP_INSTALL . - run: name: Building HTML command: | - $ACTIVATE python3 setup.py build_sphinx -b html - store_artifacts: @@ -66,7 +65,6 @@ jobs: - run: name: Building LaTeX command: | - $ACTIVATE python3 setup.py build_sphinx -b latex - run: @@ -83,7 +81,6 @@ jobs: - run: name: Checking for broken links command: | - $ACTIVATE python3 setup.py build_sphinx -b linkcheck workflows: From c31a4aea9be63001bf992971d3ee40b4ec9a059f Mon Sep 17 00:00:00 2001 From: Matthew Seal Date: Mon, 21 Oct 2019 17:49:10 -0700 Subject: [PATCH 403/671] Added initial changelog for 5.6.1 --- docs/source/changelog.rst | 48 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst index d4965809e..ef5ed7770 100644 --- a/docs/source/changelog.rst +++ b/docs/source/changelog.rst @@ -4,6 +4,54 @@ Changes in nbconvert ==================== +5.6.1 +----- + +The following authors and reviewers contributed the changes for this release -- Thanks you all! + +* Charles Frye +* Chris Holdgraf +* Felipe Rodrigues +* Gregor Sturm +* Jim +* Kerwin Sun +* Ryan Beesley +* Matthew Seal +* Matthias Geier +* thuy-van +* Tyler Makaro + +Significant Changes +~~~~~~~~~~~~~~~~~~~ + +RegExRemove applies to all cells +++++++++++++++++++++++++++++++++ + +RegExRemove preprocessor now removes cells regardless of cell outputs. Before this only cells that had outputs were filtered. + +Comprehensive notes +~~~~~~~~~~~~~~~~~~~ + +New Features +++++++++++++ +- Add support for alt tags for jpeg and png images :ghpull:`1112`: +- Allow HTML header anchor text to be HTML :ghpull:`1101`: +- Change RegExRemove to remove code cells with output :ghpull:`1095`: +- Added cell tag data attributes to HTML exporter :ghpull:`1090`: and :ghpull:`1089`: + +Fixing Problems ++++++++++++++++ +- Update svg2pdf.py to search the PATH for inkscape :ghpull:`1115`: +- Fix latex dependencies installation command for Ubuntu systems :ghpull:`1109`: + +Testing, Docs, and Builds ++++++++++++++++++++++++++ +- Added Circle CI builds for documentation :ghpull:`1114`: :ghpull:`1120`:, and :ghpull:`1116`: +- Fix typo in argument name in docstring (TagRemovePreprocessor) :ghpull:`1103`: +- Changelog typo fix :ghpull:`1100`: +- Updated API page for TagRemovePreprocessor and TemplateExporter :ghpull:`1088`: +- Added remove_input_tag traitlet to the docstring :ghpull:`1088`: + 5.6 --- From 6605e6e01fd82e1356e1afaa9f678578fa61168c Mon Sep 17 00:00:00 2001 From: Matthias Geier Date: Tue, 22 Oct 2019 20:26:36 +0200 Subject: [PATCH 404/671] CircleCI: Don't use per-branch caching --- .circleci/config.yml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 3d4699330..cbc129c48 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -21,8 +21,7 @@ jobs: - restore_cache: keys: - - v1-miniconda-{{ .Branch }}-{{ checksum "docs/environment.yml" }} - - v1-miniconda-{{ .Branch }}- + - v1-miniconda-{{ checksum "docs/environment.yml" }} - v1-miniconda- - run: @@ -43,7 +42,7 @@ jobs: conda env update -f docs/environment.yml -n root --prune - save_cache: - key: v1-miniconda-{{ .Branch }}-{{ checksum "docs/environment.yml" }} + key: v1-miniconda-{{ checksum "docs/environment.yml" }} paths: - /root/miniconda From 5d2eaed5cbba1f8dff14126c015f4fe28f5f3311 Mon Sep 17 00:00:00 2001 From: Matthew Seal Date: Thu, 24 Oct 2019 17:36:18 -0700 Subject: [PATCH 405/671] release 5.6.1 --- nbconvert/_version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nbconvert/_version.py b/nbconvert/_version.py index a8445660b..359820721 100644 --- a/nbconvert/_version.py +++ b/nbconvert/_version.py @@ -1,6 +1,6 @@ version_info = (5, 6, 1) pre_info = '' -dev_info = '.dev' +dev_info = '' def create_valid_version(release_info, epoch=None, pre_input='', dev_input=''): ''' From 3210bf7b8f55b5174c1367e3a539665799d0acb5 Mon Sep 17 00:00:00 2001 From: Matthew Seal Date: Thu, 24 Oct 2019 17:38:15 -0700 Subject: [PATCH 406/671] added dev back to version --- nbconvert/_version.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nbconvert/_version.py b/nbconvert/_version.py index 359820721..2f31face2 100644 --- a/nbconvert/_version.py +++ b/nbconvert/_version.py @@ -1,6 +1,6 @@ -version_info = (5, 6, 1) +version_info = (5, 6, 2) pre_info = '' -dev_info = '' +dev_info = '.dev' def create_valid_version(release_info, epoch=None, pre_input='', dev_input=''): ''' From 6b66e95e821ae234a5bc50cd2e066e0eb64289f2 Mon Sep 17 00:00:00 2001 From: Matthew Seal Date: Thu, 24 Oct 2019 17:39:29 -0700 Subject: [PATCH 407/671] Set dev version to 6.0.0 --- nbconvert/_version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nbconvert/_version.py b/nbconvert/_version.py index 2f31face2..9bc36898a 100644 --- a/nbconvert/_version.py +++ b/nbconvert/_version.py @@ -1,4 +1,4 @@ -version_info = (5, 6, 2) +version_info = (6, 0, 0) pre_info = '' dev_info = '.dev' From 6cdd232db6fea0233fbc0300222b76bbf57fe7a0 Mon Sep 17 00:00:00 2001 From: Sundar Date: Sat, 2 Nov 2019 13:53:01 +0000 Subject: [PATCH 408/671] Unit tests for ascii_only --- nbconvert/filters/tests/test_strings.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/nbconvert/filters/tests/test_strings.py b/nbconvert/filters/tests/test_strings.py index 0340cbb14..f436a04d5 100644 --- a/nbconvert/filters/tests/test_strings.py +++ b/nbconvert/filters/tests/test_strings.py @@ -1,3 +1,4 @@ +# -*- coding: UTF-8 -*- """ Module with tests for Strings """ @@ -18,7 +19,7 @@ from ...tests.base import TestsBase from ..strings import (wrap_text, html2text, add_anchor, strip_dollars, strip_files_prefix, get_lines, comment_lines, ipython2python, posix_path, - add_prompts, prevent_list_blocks + add_prompts, prevent_list_blocks,ascii_only ) @@ -70,6 +71,7 @@ def test_strip_dollars(self): """strip_dollars test""" tests = [ ('', ''), + (' ', ' '), ('$$', ''), ('$H$', 'H'), ('$He', 'He'), @@ -163,3 +165,14 @@ def test_prevent_list_blocks(self): ] for test in tests: self.assertEqual(prevent_list_blocks(test[0]), test[1]) + + def test_ascii_only(self): + """ascii only test""" + tests = [ + ('', ''), + (' ', ' '), + ('Hello', 'Hello'), + ('Hello 中文', 'Hello ??????'), + ] + for test in tests: + self.assertEqual(test[1], ascii_only(test[0])) \ No newline at end of file From 398a265932a2f95d8199f3d19cba100a32bf6138 Mon Sep 17 00:00:00 2001 From: Sundar Date: Sat, 2 Nov 2019 16:32:41 +0000 Subject: [PATCH 409/671] fix the assert to make it work in python version 3 --- nbconvert/filters/tests/test_strings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nbconvert/filters/tests/test_strings.py b/nbconvert/filters/tests/test_strings.py index f436a04d5..b4fa4f2cf 100644 --- a/nbconvert/filters/tests/test_strings.py +++ b/nbconvert/filters/tests/test_strings.py @@ -172,7 +172,7 @@ def test_ascii_only(self): ('', ''), (' ', ' '), ('Hello', 'Hello'), - ('Hello 中文', 'Hello ??????'), + ('Hello 中文', 'Hello ??'), ] for test in tests: self.assertEqual(test[1], ascii_only(test[0])) \ No newline at end of file From 2b51b4372e6409457f7c0422829efe7d5e1feb33 Mon Sep 17 00:00:00 2001 From: Florian Rathgeber Date: Sat, 2 Nov 2019 18:47:58 +0000 Subject: [PATCH 410/671] Fix malformed log message in exporter._preprocess, closes #955 --- nbconvert/exporters/exporter.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/nbconvert/exporters/exporter.py b/nbconvert/exporters/exporter.py index e6160cf9b..dba0015ef 100644 --- a/nbconvert/exporters/exporter.py +++ b/nbconvert/exporters/exporter.py @@ -310,14 +310,14 @@ def _preprocess(self, nb, resources): nbc = copy.deepcopy(nb) resc = copy.deepcopy(resources) - #Run each preprocessor on the notebook. Carry the output along - #to each preprocessor + # Run each preprocessor on the notebook. Carry the output along + # to each preprocessor for preprocessor in self._preprocessors: nbc, resc = preprocessor(nbc, resc) try: nbformat.validate(nbc, relax_add_props=True) except nbformat.ValidationError: - self.log.error('Notebook is invalid after preprocessor {}', + self.log.error('Notebook is invalid after preprocessor %s', preprocessor) raise From 549c83319df1fd38996b5eb14dae5577bbed4fcb Mon Sep 17 00:00:00 2001 From: jon Date: Sat, 2 Nov 2019 19:48:08 +0000 Subject: [PATCH 411/671] Adding a more descriptive TimeoutError message; fixes #1123 --- nbconvert/preprocessors/execute.py | 37 ++++++++++++++++--- nbconvert/preprocessors/tests/test_execute.py | 9 ++++- 2 files changed, 39 insertions(+), 7 deletions(-) diff --git a/nbconvert/preprocessors/execute.py b/nbconvert/preprocessors/execute.py index 81988b3f3..f5cda0ba2 100644 --- a/nbconvert/preprocessors/execute.py +++ b/nbconvert/preprocessors/execute.py @@ -28,9 +28,19 @@ from .base import Preprocessor from ..utils.exceptions import ConversionException + +class CellTimeoutError(TimeoutError): + @classmethod + def error_from_timeout_and_cell(cls, msg, timeout, cell): + src_by_lines = cell.source.strip().split("\n") + src = cell.source if len(src_by_lines) < 11 else "{}\n...\n{}".format(src_by_lines[:5], src_by_lines[-5:]) + return cls(timeout_err_msg.format(timeout=timeout, msg=msg, cell_contents=src)) + + class DeadKernelError(RuntimeError): pass + class CellExecutionComplete(Exception): """ Used as a control signal for cell execution across run_cell and @@ -40,6 +50,7 @@ class CellExecutionComplete(Exception): """ pass + class CellExecutionError(ConversionException): """ Custom exception to propagate exceptions that are raised during @@ -71,6 +82,7 @@ def from_cell_and_msg(cls, cell, msg): evalue=msg.get('evalue', '') )) + exec_err_msg = u"""\ An error occurred while executing the following cell: ------------------ @@ -81,6 +93,17 @@ def from_cell_and_msg(cls, cell, msg): {ename}: {evalue} """ + +timeout_err_msg = u"""\ +A cell timed out while it was being executed, after {timeout} seconds. +The message was: {msg}. +Here is a preview of the cell contents: +------------------- +{cell_contents} +------------------- +""" + + class ExecutePreprocessor(Preprocessor): """ Executes all the cells in a notebook @@ -494,14 +517,14 @@ def _get_timeout(self, cell): return timeout - def _handle_timeout(self): + def _handle_timeout(self, timeout, cell=None): self.log.error( - "Timeout waiting for execute reply (%is)." % self.timeout) + "Timeout waiting for execute reply (%is)." % timeout) if self.interrupt_on_timeout: self.log.error("Interrupting kernel") self.km.interrupt_kernel() else: - raise TimeoutError("Cell execution timed out") + raise CellTimeoutError.error_from_timeout_and_cell("Cell execution timed out", timeout, cell) def _check_alive(self): if not self.kc.is_alive(): @@ -521,7 +544,7 @@ def _wait_for_reply(self, msg_id, cell=None): self._check_alive() cummulative_time += timeout_interval if timeout and cummulative_time > timeout: - self._handle_timeout() + self._handle_timeout(timeout, cell) break else: if msg['parent_header'].get('msg_id') == msg_id: @@ -538,7 +561,6 @@ def _timeout_with_deadline(self, timeout, deadline): def _passed_deadline(self, deadline): if deadline is not None and deadline - monotonic() <= 0: - self._handle_timeout() return True return False @@ -569,6 +591,7 @@ def run_cell(self, cell, cell_index=0, store_history=False): while more_output or polling_exec_reply: if polling_exec_reply: if self._passed_deadline(deadline): + self._handle_timeout(exec_timeout, cell) polling_exec_reply = False continue @@ -594,7 +617,9 @@ def run_cell(self, cell, cell_index=0, store_history=False): continue if self.raise_on_iopub_timeout: - raise TimeoutError("Timeout waiting for IOPub output") + raise CellTimeoutError.error_from_timeout_and_cell( + "Timeout waiting for IOPub output", self.iopub_timeout, cell + ) else: self.log.warning("Timeout waiting for IOPub output") more_output = False diff --git a/nbconvert/preprocessors/tests/test_execute.py b/nbconvert/preprocessors/tests/test_execute.py index 243a725da..6eff5bd7c 100644 --- a/nbconvert/preprocessors/tests/test_execute.py +++ b/nbconvert/preprocessors/tests/test_execute.py @@ -387,8 +387,15 @@ def test_timeout(self): res = self.build_resources() res['metadata']['path'] = os.path.dirname(filename) - with pytest.raises(TimeoutError): + with pytest.raises(TimeoutError) as err: run_notebook(filename, dict(timeout=1), res) + self.assertEqual(str(err.value.args[0]), """A cell timed out while it was being executed, after 1 seconds. +The message was: Cell execution timed out. +Here is a preview of the cell contents: +------------------- +while True: continue +------------------- +""") def test_timeout_func(self): """Check that an error is raised when a computation times out""" From 4dfae390275eedc848faecbaf33621dd58b60bb9 Mon Sep 17 00:00:00 2001 From: Thomas Kluyver Date: Sun, 3 Nov 2019 21:55:13 +0100 Subject: [PATCH 412/671] Use nbsphinx development version for docs build Should work with recent changes to nbconvert templates. --- docs/environment.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/environment.yml b/docs/environment.yml index a418f89b8..c2fa94854 100644 --- a/docs/environment.yml +++ b/docs/environment.yml @@ -12,5 +12,5 @@ dependencies: - tornado - entrypoints - pip: - - nbsphinx>=0.2.12 + - git+https://github.com/spatialaudio/nbsphinx.git#egg=nbsphinx - sphinxcontrib_github_alt From e8f5377d7ac031416878aa388fd73dd0ded81ff6 Mon Sep 17 00:00:00 2001 From: Jon Bannister Date: Mon, 4 Nov 2019 10:23:47 +0000 Subject: [PATCH 413/671] Adding defence against cell=None in CellTimeoutError --- nbconvert/preprocessors/execute.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/nbconvert/preprocessors/execute.py b/nbconvert/preprocessors/execute.py index f5cda0ba2..5286cae28 100644 --- a/nbconvert/preprocessors/execute.py +++ b/nbconvert/preprocessors/execute.py @@ -30,10 +30,16 @@ class CellTimeoutError(TimeoutError): + """ + A custom exception to capture when a cell has timed out during execution. + """ @classmethod def error_from_timeout_and_cell(cls, msg, timeout, cell): - src_by_lines = cell.source.strip().split("\n") - src = cell.source if len(src_by_lines) < 11 else "{}\n...\n{}".format(src_by_lines[:5], src_by_lines[-5:]) + if cell and cell.source: + src_by_lines = cell.source.strip().split("\n") + src = cell.source if len(src_by_lines) < 11 else "{}\n...\n{}".format(src_by_lines[:5], src_by_lines[-5:]) + else: + src = "Cell contents not found." return cls(timeout_err_msg.format(timeout=timeout, msg=msg, cell_contents=src)) From df7600d71363a71337dc814b886e8267fab79ca4 Mon Sep 17 00:00:00 2001 From: Thomas Kluyver Date: Mon, 4 Nov 2019 22:36:49 +0100 Subject: [PATCH 414/671] Remove Python 2 from Travis test configuration --- .travis.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 34d144cf5..8e299af8b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,7 +7,6 @@ language: python matrix: include: - - python: 2.7 - python: 3.5 - python: 3.6 - python: 3.7 From 1e8c53755ed7e88d99a292a72eeae118cf19f713 Mon Sep 17 00:00:00 2001 From: Thomas Kluyver Date: Mon, 4 Nov 2019 22:38:54 +0100 Subject: [PATCH 415/671] Remove Python 2.7 support in setup.py --- setup.py | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/setup.py b/setup.py index 2dcf91a1a..7b47e3f1b 100644 --- a/setup.py +++ b/setup.py @@ -16,13 +16,11 @@ import sys v = sys.version_info -if v[:2] < (2,7) or (v[0] >= 3 and v[:2] < (3,5)): - error = "ERROR: %s requires Python version 2.7 or 3.5 or above." % name +if v[:2] < (3, 5): + error = "ERROR: %s requires Python version 3.5 or above." % name print(error, file=sys.stderr) sys.exit(1) -PY3 = (sys.version_info[0] >= 3) - #----------------------------------------------------------------------------- # get on with it #----------------------------------------------------------------------------- @@ -35,10 +33,7 @@ from setuptools.command.develop import develop from io import BytesIO -try: - from urllib.request import urlopen -except ImportError: - from urllib import urlopen +from urllib.request import urlopen from distutils.cmd import Command from distutils.command.build import build @@ -211,7 +206,7 @@ def get_data_files(): package_data = package_data, data_files = get_data_files(), cmdclass = cmdclass, - python_requires = '>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*', + python_requires = '>=3.5', author = 'Jupyter Development Team', author_email = 'jupyter@googlegroups.com', url = 'https://jupyter.org', @@ -230,7 +225,6 @@ def get_data_files(): 'Intended Audience :: Science/Research', 'License :: OSI Approved :: BSD License', 'Programming Language :: Python', - 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', From a0fe8a07bc6b559720e374f2bedbb38233cc5c9d Mon Sep 17 00:00:00 2001 From: letmerecall Date: Wed, 6 Nov 2019 23:34:23 +0530 Subject: [PATCH 416/671] Fix #1136, typo in customizing doc --- docs/source/customizing.ipynb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/customizing.ipynb b/docs/source/customizing.ipynb index ec780c74a..8ece56d60 100644 --- a/docs/source/customizing.ipynb +++ b/docs/source/customizing.ipynb @@ -84,7 +84,7 @@ "source": [ "From the code, you can see that non-code cells are also exported. If you wanted to change that behaviour, you would first look to nbconvert [configuration options page](./config_options.rst) to see if there is an option available that can give you your desired behaviour. \n", "\n", - "In this case, if you wanted to remove code cells from the output, you could use the `TemplateExporter.exclude_markdown` traitlet directly, as below. " + "In this case, if you wanted to remove non-code cells from the output, you could use the `TemplateExporter.exclude_markdown` traitlet directly, as below. " ] }, { From faac2575587aae7e7effc5e2c138fa7aa80dbe03 Mon Sep 17 00:00:00 2001 From: Sylvain Corlay Date: Wed, 13 Nov 2019 23:26:36 +0100 Subject: [PATCH 417/671] Update nbconvert template --- .../nbconvert/templates/lab/base.html.j2 | 21 +++++++++++-------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/share/jupyter/nbconvert/templates/lab/base.html.j2 b/share/jupyter/nbconvert/templates/lab/base.html.j2 index eb9c45feb..e7abd2574 100644 --- a/share/jupyter/nbconvert/templates/lab/base.html.j2 +++ b/share/jupyter/nbconvert/templates/lab/base.html.j2 @@ -3,9 +3,12 @@ {% block codecell %} {%- if not cell.outputs -%} -{%- set extra_class="jp-mod-noOutput" -%} +{%- set no_output_class="jp-mod-noOutputs" -%} {%- endif -%} - {%- endblock -%} -{# +{# ############################################################################### # TODO: how to better handle JavaScript repr? # ############################################################################### @@ -231,8 +234,8 @@ var element_id = '#{{ div_id }}'; {%- block data_widget_state scoped %} {% set div_id = uuid4() %} -{% set datatype_list = output.data | filter_data_type %} -{% set datatype = datatype_list[0]%} +{% set datatype_list = output.data | filter_data_type %} +{% set datatype = datatype_list[0]%}
{%- endblock html_head_js_jquery -%} {%- block html_head_js_requirejs -%} diff --git a/share/jupyter/nbconvert/templates/lab/index.html.j2 b/share/jupyter/nbconvert/templates/lab/index.html.j2 index e1c7bec00..7d3e07469 100644 --- a/share/jupyter/nbconvert/templates/lab/index.html.j2 +++ b/share/jupyter/nbconvert/templates/lab/index.html.j2 @@ -1,6 +1,11 @@ {%- extends 'classic/index.html.j2' -%} +{%- block html_head_js_jquery -%} +{# no jQuery in Jupyter lab, so also not in this template #} +{%- endblock html_head_js_jquery -%} + + {% block notebook_css %} {{ resources.include_css("static/index.css") }} {% if resources.theme == 'dark' %} From 76b4f84b73f2b71d49af11f9caf75f8c9c97dfee Mon Sep 17 00:00:00 2001 From: "Maarten A. Breddels" Date: Fri, 13 Dec 2019 16:03:29 +0100 Subject: [PATCH 421/671] fix: restore jQuery element variable instead of element_id --- share/jupyter/nbconvert/templates/base/base.html.j2 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/share/jupyter/nbconvert/templates/base/base.html.j2 b/share/jupyter/nbconvert/templates/base/base.html.j2 index 4252d571c..81e352383 100644 --- a/share/jupyter/nbconvert/templates/base/base.html.j2 +++ b/share/jupyter/nbconvert/templates/base/base.html.j2 @@ -248,7 +248,7 @@ var element = $('#{{ div_id }}');
@@ -239,7 +239,7 @@ var element_id = '#{{ div_id }}';
@@ -245,8 +243,7 @@ var element = $('#{{ div_id }}'); {% set div_id = uuid4() %} {% set datatype_list = output.data | filter_data_type %} {% set datatype = datatype_list[0]%} -
-
+
diff --git a/share/jupyter/nbconvert/templates/lab/base.html.j2 b/share/jupyter/nbconvert/templates/lab/base.html.j2 index 292e82d0c..b9d99e255 100644 --- a/share/jupyter/nbconvert/templates/lab/base.html.j2 +++ b/share/jupyter/nbconvert/templates/lab/base.html.j2 @@ -223,8 +223,7 @@ class="unconfined" {% set div_id = uuid4() %} {%- block data_javascript scoped %} -
-