diff --git a/IPython/nbconvert/utils/pandoc.py b/IPython/nbconvert/utils/pandoc.py index 248309a..af5568f 100644 --- a/IPython/nbconvert/utils/pandoc.py +++ b/IPython/nbconvert/utils/pandoc.py @@ -1,6 +1,6 @@ """Utility for calling pandoc""" #----------------------------------------------------------------------------- -# Copyright (c) 2013 the IPython Development Team. +# Copyright (c) 2014 the IPython Development Team. # # Distributed under the terms of the Modified BSD License. # @@ -10,33 +10,29 @@ #----------------------------------------------------------------------------- # Imports #----------------------------------------------------------------------------- - from __future__ import print_function # Stdlib imports import subprocess +import re +import warnings from io import TextIOWrapper, BytesIO # IPython imports from IPython.utils.py3compat import cast_bytes +from IPython.utils.version import check_version +from IPython.utils.process import is_cmd_found, FindCmdError from .exceptions import ConversionException #----------------------------------------------------------------------------- # Classes and functions #----------------------------------------------------------------------------- - -class PandocMissing(ConversionException): - """Exception raised when Pandoc is missing. """ - pass - +_minimal_version = "1.12.1" def pandoc(source, fmt, to, extra_args=None, encoding='utf-8'): """Convert an input string in format `from` to format `to` via pandoc. - This function will raise an error if pandoc is not installed. - Any error messages generated by pandoc are printed to stderr. - Parameters ---------- source : string @@ -50,21 +46,83 @@ def pandoc(source, fmt, to, extra_args=None, encoding='utf-8'): ------- out : unicode Output as returned by pandoc. + + Exceptions + ---------- + This function will raise PandocMissing if pandoc is not installed. + Any error messages generated by pandoc are printed to stderr. + """ - command = ['pandoc', '-f', fmt, '-t', to] + cmd = ['pandoc', '-f', fmt, '-t', to] if extra_args: - command.extend(extra_args) - try: - p = subprocess.Popen(command, - stdin=subprocess.PIPE, stdout=subprocess.PIPE - ) - except OSError as e: - raise PandocMissing( - "The command '%s' returned an error: %s.\n" %(" ".join(command), e) + - "Please check that pandoc is installed:\n" + - "http://johnmacfarlane.net/pandoc/installing.html" - ) + cmd.extend(extra_args) + + # this will raise an exception that will pop us out of here + check_pandoc_version() + + # we can safely continue + p = subprocess.Popen(cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE) out, _ = p.communicate(cast_bytes(source, encoding)) out = TextIOWrapper(BytesIO(out), encoding, 'replace').read() return out.rstrip('\n') + +def get_pandoc_version(): + """Gets the Pandoc version if Pandoc is installed. + + Return + ------ + If the minimal version is not met, it will probe Pandoc for its version, cache it and return that value. + If the minimal version is met, it will return the cached version and stop probing Pandoc + (unless `clean_cache()` is called). + + Exceptions + ---------- + PandocMissing will be raised if pandoc is unavailable. + """ + global __version + + if __version is None: + if not is_cmd_found('pandoc'): + raise PandocMissing() + + out = subprocess.check_output( ['pandoc', '-v'], universal_newlines=True) + pv_re = re.compile(r'(\d{0,3}\.\d{0,3}\.\d{0,3})') + __version = pv_re.search(out).group(0) + return __version + + +def check_pandoc_version(): + """Returns True if minimal pandoc version is met. + + Exceptions + ---------- + PandocMissing will be raised if pandoc is unavailable. + """ + v = get_pandoc_version() + ok = check_version(v , _minimal_version ) + if not ok: + warnings.warn( "You are using an old version of pandoc (%s)\n" % v + + "Recommended version is %s.\nTry updating." % _minimal_version + + "http://johnmacfarlane.net/pandoc/installing.html.\nContinuing with doubts...", + RuntimeWarning, stacklevel=2) + return ok + +#----------------------------------------------------------------------------- +# Exception handling +#----------------------------------------------------------------------------- +class PandocMissing(ConversionException): + """Exception raised when Pandoc is missing. """ + def __init__(self, *args, **kwargs): + super(PandocMissing, self).__init__( "Pandoc wasn't found.\n" + + "Please check that pandoc is installed:\n" + + "http://johnmacfarlane.net/pandoc/installing.html" ) + +#----------------------------------------------------------------------------- +# Internal state management +#----------------------------------------------------------------------------- +def clean_cache(): + global __version + __version = None + +__version = None diff --git a/IPython/nbconvert/utils/tests/__init__.py b/IPython/nbconvert/utils/tests/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/IPython/nbconvert/utils/tests/__init__.py diff --git a/IPython/nbconvert/utils/tests/test_pandoc.py b/IPython/nbconvert/utils/tests/test_pandoc.py new file mode 100644 index 0000000..b1eec9a --- /dev/null +++ b/IPython/nbconvert/utils/tests/test_pandoc.py @@ -0,0 +1,62 @@ +"""Test Pandoc module""" +#----------------------------------------------------------------------------- +# Copyright (C) 2014 The IPython Development Team +# +# Distributed under the terms of the BSD License. The full license is in +# the file COPYING, distributed as part of this software. +#----------------------------------------------------------------------------- + +#----------------------------------------------------------------------------- +# Imports +#----------------------------------------------------------------------------- +import os + +from IPython.testing import decorators as dec + +from IPython.nbconvert.tests.base import TestsBase +from .. import pandoc + +#----------------------------------------------------------------------------- +# Classes and functions +#----------------------------------------------------------------------------- +class TestPandoc(TestsBase): + """Collection of Pandoc tests""" + + def __init__(self, *args, **kwargs): + super(TestPandoc, self).__init__(*args, **kwargs) + self.original_env = os.environ.copy() + + @dec.onlyif_cmds_exist('pandoc') + def test_pandoc_available(self): + """ Test behaviour that pandoc functions raise PandocMissing as documented """ + pandoc.clean_cache() + + os.environ["PATH"] = "" + assert pandoc_function_raised_missing(pandoc.get_pandoc_version) == True + assert pandoc_function_raised_missing(pandoc.check_pandoc_version) == True + assert pandoc_function_raised_missing(pandoc.pandoc, "", "markdown", "html") == True + + # original_env["PATH"] should contain pandoc + os.environ["PATH"] = self.original_env["PATH"] + assert pandoc_function_raised_missing(pandoc.get_pandoc_version) == False + assert pandoc_function_raised_missing(pandoc.check_pandoc_version) == False + assert pandoc_function_raised_missing(pandoc.pandoc, "", "markdown", "html") == False + + @dec.onlyif_cmds_exist('pandoc') + def test_minimal_version(self): + original_minversion = pandoc._minimal_version + + pandoc._minimal_version = "120.0" + assert not pandoc.check_pandoc_version() + + pandoc._minimal_version = pandoc.get_pandoc_version() + assert pandoc.check_pandoc_version() + + +def pandoc_function_raised_missing(f, *args, **kwargs): + try: + f(*args, **kwargs) + except pandoc.PandocMissing: + return True + else: + return False diff --git a/docs/source/whatsnew/pr/minimal-pandoc.rst b/docs/source/whatsnew/pr/minimal-pandoc.rst new file mode 100644 index 0000000..0a799c1 --- /dev/null +++ b/docs/source/whatsnew/pr/minimal-pandoc.rst @@ -0,0 +1,3 @@ +* PandocMissing exceptions will be raised if Pandoc is unavailable +* Recommended Pandoc version for use with nbconvert is 1.12.1 +* warnings will be printed if the version is not good.