Repository: mwouts/jupytext
Pull request #273 (2019-06-30 19:38:50)

Jupytext execute
- New `--execute` option in Jupytext CLI (#231)
- The `--set-formats` option in Jupytext CLI also triggers `--sync`, allowing shorter commands.

codecov[bot]: # [Codecov](https://codecov.io/gh/mwouts/jupytext/pull/273?src=pr&el=h1) Report
> Merging [#273](https://codecov.io/gh/mwouts/jupytext/pull/273?src=pr&el=desc) into [master](https://codecov.io/gh/mwouts/jupytext/commit/ab4d11653bc0635798df64714082c56a7588b2b9?src=pr&el=desc) will **decrease** coverage by `0.06%`.
> The diff coverage is `88.77%`.

[![Impacted file tree graph](https://codecov.io/gh/mwouts/jupytext/pull/273/graphs/tree.svg?width=650&token=a1pNjsiILi&height=150&src=pr)](https://codecov.io/gh/mwouts/jupytext/pull/273?src=pr&el=tree)

```diff
@@            Coverage Diff             @@
##           master     #273      +/-   ##
==========================================
- Coverage   99.02%   98.95%   -0.07%     
==========================================
  Files          66       67       +1     
  Lines        6552     6632      +80     
==========================================
+ Hits         6488     6563      +75     
- Misses         64       69       +5
```


| [Impacted Files](https://codecov.io/gh/mwouts/jupytext/pull/273?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [tests/test\_cli.py](https://codecov.io/gh/mwouts/jupytext/pull/273/diff?src=pr&el=tree#diff-dGVzdHMvdGVzdF9jbGkucHk=) | `100% <100%> (ø)` | :arrow_up: |
| [jupytext/jupytext.py](https://codecov.io/gh/mwouts/jupytext/pull/273/diff?src=pr&el=tree#diff-anVweXRleHQvanVweXRleHQucHk=) | `98.02% <100%> (ø)` | :arrow_up: |
| [tests/test\_read\_simple\_markdown.py](https://codecov.io/gh/mwouts/jupytext/pull/273/diff?src=pr&el=tree#diff-dGVzdHMvdGVzdF9yZWFkX3NpbXBsZV9tYXJrZG93bi5weQ==) | `100% <100%> (ø)` | :arrow_up: |
| [jupytext/cli.py](https://codecov.io/gh/mwouts/jupytext/pull/273/diff?src=pr&el=tree#diff-anVweXRleHQvY2xpLnB5) | `95.8% <100%> (+0.15%)` | :arrow_up: |
| [tests/utils.py](https://codecov.io/gh/mwouts/jupytext/pull/273/diff?src=pr&el=tree#diff-dGVzdHMvdXRpbHMucHk=) | `100% <100%> (ø)` | :arrow_up: |
| [jupytext/languages.py](https://codecov.io/gh/mwouts/jupytext/pull/273/diff?src=pr&el=tree#diff-anVweXRleHQvbGFuZ3VhZ2VzLnB5) | `100% <100%> (+12.5%)` | :arrow_up: |
| [jupytext/kernels.py](https://codecov.io/gh/mwouts/jupytext/pull/273/diff?src=pr&el=tree#diff-anVweXRleHQva2VybmVscy5weQ==) | `83.33% <50%> (-8.34%)` | :arrow_down: |
| [tests/test\_execute.py](https://codecov.io/gh/mwouts/jupytext/pull/273/diff?src=pr&el=tree#diff-dGVzdHMvdGVzdF9leGVjdXRlLnB5) | `85.18% <85.18%> (ø)` | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/mwouts/jupytext/pull/273?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/mwouts/jupytext/pull/273?src=pr&el=footer). Last update [ab4d116...aa3ad40](https://codecov.io/gh/mwouts/jupytext/pull/273?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).


Patch:
diff --git a/HISTORY.rst b/HISTORY.rst
index 1a5f3409..99e9b539 100644
--- a/HISTORY.rst
+++ b/HISTORY.rst
@@ -9,7 +9,8 @@ Release History
 **Improvements**
 
 - ``jupytext``'s ``read`` and ``write`` functions can be used as drop-in remplacements for ``nbformat``'s ones (#262).
-
+- New ``--execute`` option in Jupytext CLI (#231)
+- The ``--set-formats`` option in Jupytext CLI also triggers ``--sync``, allowing shorter commands.
 
 1.1.7 (2019-06-23)
 ++++++++++++++++++++++
diff --git a/README.md b/README.md
index adba4305..e9861370 100755
--- a/README.md
+++ b/README.md
@@ -111,9 +111,12 @@ For more information see [the jupytext documentation](https://jupytext.readthedo
 The package provides a `jupytext` script for command line conversion between the various notebook extensions:
 
 ```bash
-jupytext --to py notebook.ipynb                 # create a notebook.py file in the light format
-jupytext --to notebook notebook.py              # overwrite notebook.ipynb (remove outputs)
-jupytext --update --to notebook notebook.py     # update notebook.ipynb (preserve outputs and metadata)
+jupytext --to py notebook.ipynb                 # convert notebook.ipynb to a .py file
+jupytext --to notebook notebook.py              # convert notebook.py to an .ipynb file with no outputs
+jupytext --to notebook --execute notebook.md    # convert notebook.md to an .ipynb file and run it 
+jupytext --update --to notebook notebook.py     # update the input cells in the .ipynb file and preserve outputs and metadata
+jupytext --set-formats --ipynb,py notebook.ipynb  # Turn notebook.ipynb into a paired ipynb/py notebook
+jupytext --sync notebook.ipynb                  # Update all paired representations of notebook.ipynb
 ```
 
 For more examples, see the [jupytext documentation](https://jupytext.readthedocs.io)
diff --git a/docs/using-cli.md b/docs/using-cli.md
index e0b2ba38..43c90877 100644
--- a/docs/using-cli.md
+++ b/docs/using-cli.md
@@ -5,15 +5,14 @@
 The package provides a `jupytext` script for command line conversion between the various notebook extensions:
 
 ```bash
-jupytext --to py notebook.ipynb                 # create a notebook.py file in the light format
-jupytext --to py:percent notebook.ipynb         # create a notebook.py file in the double percent format
-jupytext --to py:percent --comment-magics false notebook.ipynb   # create a notebook.py file in the double percent format, and do not comment magic commands
-jupytext --to markdown notebook.ipynb           # create a notebook.md file
-jupytext --output script.py notebook.ipynb      # create a script.py file
+jupytext --to py notebook.ipynb                 # convert notebook.ipynb to a .py file
+jupytext --to py:percent notebook.ipynb         # convert notebook.ipynb to a .py file in the double percent format
+jupytext --to py:percent --comment-magics false notebook.ipynb   # same as above + do not comment magic commands
+jupytext --to markdown notebook.ipynb           # convert notebook.ipynb to a .md file
+jupytext --output script.py notebook.ipynb      # convert notebook.ipynb to a script.py file
 
-jupytext --to notebook notebook.py              # overwrite notebook.ipynb (remove outputs)
-jupytext --to notebook --update notebook.py     # update notebook.ipynb (preserve outputs)
-jupytext --to ipynb notebook1.md notebook2.py   # overwrite notebook1.ipynb and notebook2.ipynb
+jupytext --to notebook notebook.py              # convert notebook.py to an .ipynb file with no outputs
+jupytext --update --to notebook notebook.py     # update the input cells in the .ipynb file and preserve outputs and metadata
 
 jupytext --to md --test notebook.ipynb          # Test round trip conversion
 
@@ -21,12 +20,30 @@ jupytext --to md --output - notebook.ipynb      # display the markdown version o
 jupytext --from ipynb --to py:percent           # read ipynb from stdin and write double percent script on stdout
 ```
 
-Jupytext has a `--sync` mode that updates all the paired representations of a notebook based on the file that was last modified. You may also find useful to `--pipe` the text representation of a notebook into tools like `black`:
+Jupytext has a `--sync` mode that updates all the paired representations of a notebook based on timestamps: 
+```bash
+jupytext --set-formats --ipynb,py notebook.ipynb  # Turn notebook.ipynb into a paired ipynb/py notebook
+jupytext --sync notebook.ipynb                    # Update whichever of notebook.ipynb/notebook.py is outdated
+```
+
+For convenience, when creating a notebook from text you can execute it:
+```bash
+jupytext --set-kernel - notebook.md             # set a kernel metadata on the given notebook that points to the current python executable 
+jupytext --to notebook --execute notebook.md    # convert notebook.md to an .ipynb file and run it 
+```
+
+If you wanted to convert a collection of Markdown files to paired notebooks, and execute them in the current Python environment, you could run:
+```bash
+jupytext --set-formats ipynb,md --execute *.md 
+```
+
+You may also find useful to `--pipe` the text representation of a notebook into tools like `black`:
 ```bash
 jupytext --sync --pipe black notebook.ipynb    # read most recent version of notebook, reformat with black, save
 ```
 
-The `jupytext` command accepts many arguments. Use the `--set-formats` and the `--update-metadata` arguments to edit the pairing information or more generally the notebook metadata. Execute `jupytext --help` to access the documentation.
+
+Execute `jupytext --help` to access the full documentation.
 
 ## Jupytext as a Git pre-commit hook
 
diff --git a/jupytext/cli.py b/jupytext/cli.py
index 755ac9cf..12fb6fbf 100644
--- a/jupytext/cli.py
+++ b/jupytext/cli.py
@@ -15,7 +15,7 @@ from .header import recursive_update
 from .paired_paths import paired_paths, base_path, full_path, InconsistentPath
 from .combine import combine_inputs_with_outputs
 from .compare import test_round_trip_conversion, NotebookDifference
-from .kernels import kernelspec_from_language, find_kernel_specs, get_kernel_spec
+from .kernels import kernelspec_from_language, find_kernel_specs, get_kernel_spec, ExecutePreprocessor
 from .version import __version__
 
 
@@ -70,7 +70,8 @@ def parse_jupytext_args(args=None):
     parser.add_argument('--set-formats',
                         type=str,
                         help='Set jupytext.formats metadata to the given value. Use this to activate pairing on a '
-                             'notebook, with e.g. --set-formats ipynb,py:light')
+                             'notebook, with e.g. --set-formats ipynb,py:light. The --set-formats option also triggers '
+                             'the creation/update of all paired files')
     parser.add_argument('--set-kernel', '-k',
                         type=str,
                         help="Set the kernel with the given name on the notebook. Use '--set-kernel -' to set "
@@ -139,6 +140,11 @@ def parse_jupytext_args(args=None):
                         help='The format in which the notebook should be piped to other programs, when using the '
                              '--pipe and/or --check commands.')
 
+    # Execute the notebook
+    parser.add_argument('--execute',
+                        action='store_true',
+                        help='Execute the notebook with the given kernel')
+
     parser.add_argument('--quiet', '-q',
                         action='store_true',
                         default=False,
@@ -190,6 +196,7 @@ def jupytext(args=None):
         # Replace empty string with None
         args.update_metadata = recursive_update(args.update_metadata,
                                                 {'jupytext': {'formats': args.set_formats or None}})
+        args.sync = True
 
     if args.paired_paths:
         if len(args.notebooks) != 1:
@@ -268,9 +275,14 @@ def jupytext(args=None):
                 if not args.output and nb_file != '-':
                     nb_dest = full_path(base_path(nb_file, args.input_format), dest_fmt)
 
-                # Set the kernel
-            if args.set_kernel:
-                if args.set_kernel == '-':
+            # Set the kernel
+            set_kernel = args.set_kernel
+            if args.execute and notebook.metadata.get('kernelspec', {}).get('name') is None:
+                log("[jupytext] Setting default kernel with --set-kernel -")
+                set_kernel = '-'
+
+            if set_kernel:
+                if set_kernel == '-':
                     language = notebook.metadata.get('jupytext', {}).get('main_language') \
                                or notebook.metadata['kernelspec']['language']
                     if not language:
@@ -281,7 +293,7 @@ def jupytext(args=None):
                         raise ValueError('Found no kernel for {}'.format(language))
                 else:
                     try:
-                        kernelspec = get_kernel_spec(args.set_kernel)
+                        kernelspec = get_kernel_spec(set_kernel)
                     except KeyError:
                         raise KeyError('Please choose a kernel name among {}'
                                        .format([name for name in find_kernel_specs()]))
@@ -317,8 +329,15 @@ def jupytext(args=None):
             for cmd in args.check or []:
                 pipe_notebook(notebook, cmd, args.pipe_fmt, update=False)
 
+            # Execute the notebook
+            if args.execute:
+                log("[jupytext] Executing notebook")
+                kernel_name = notebook.metadata.get('kernelspec', {}).get('name')
+                ep = ExecutePreprocessor(timeout=None, kernel_name=kernel_name)
+                ep.preprocess(notebook, resources={})
+
             # III. ### Possible actions ###
-            modified = args.update_metadata or args.pipe or args.set_kernel
+            modified = args.update_metadata or args.pipe or args.execute
             # a. Test round trip conversion
             if args.test or args.test_strict:
                 try:
@@ -487,7 +506,7 @@ def load_paired_notebook(notebook, fmt, nb_file, log):
     return notebook, latest_inputs, latest_outputs
 
 
-def pipe_notebook(notebook, command, fmt='py:percent', update=True, preserve_outputs=True):
+def pipe_notebook(notebook, command, fmt='py:percent', update=True):
     """Pipe the notebook, in the desired representation, to the given command. Update the notebook
     with the returned content if desired."""
     if command in ['black', 'flake8', 'autopep8']:
@@ -512,7 +531,7 @@ def pipe_notebook(notebook, command, fmt='py:percent', update=True, preserve_out
 
     piped_notebook = reads(cmd_output.decode('utf-8'), fmt)
 
-    if preserve_outputs:
+    if fmt['extension'] != '.ipynb':
         combine_inputs_with_outputs(piped_notebook, notebook, fmt)
 
     # Remove jupytext / text_representation entry
diff --git a/jupytext/jupytext.py b/jupytext/jupytext.py
index f69169b5..29e12f82 100644
--- a/jupytext/jupytext.py
+++ b/jupytext/jupytext.py
@@ -120,7 +120,7 @@ class TextNotebookConverter(NotebookReader, NotebookWriter):
             cells=nb.cells)
 
         metadata = nb.metadata
-        default_language = default_language_from_metadata_and_ext(metadata, self.implementation.extension)
+        default_language = default_language_from_metadata_and_ext(metadata, self.implementation.extension) or 'python'
         self.update_fmt_with_notebook_options(nb.metadata)
 
         if 'main_language' in metadata.get('jupytext', {}):
diff --git a/jupytext/kernels.py b/jupytext/kernels.py
index 60d3529e..6483b451 100644
--- a/jupytext/kernels.py
+++ b/jupytext/kernels.py
@@ -2,21 +2,28 @@
 
 import sys
 
+
+def raise_error(error):
+    """Return a function that raises the given error when evaluated"""
+
+    def local_function(*args, **kwargs):
+        raise error
+
+    return local_function
+
+
 try:
     # I prefer not to take a dependency on jupyter_client
     from jupyter_client.kernelspec import find_kernel_specs, get_kernel_spec
 except ImportError as err:
-    def raise_error(error):
-        """Return a function that raises the given error when evaluated"""
-
-        def local_function(*args, **kwargs):
-            raise error
-
-        return local_function
-
     find_kernel_specs = raise_error(err)
     get_kernel_spec = raise_error(err)
 
+try:
+    from nbconvert.preprocessors import ExecutePreprocessor
+except ImportError as err:
+    ExecutePreprocessor = raise_error(err)
+
 
 def set_kernelspec_from_language(notebook):
     """Set the kernel specification based on the 'main_language' metadata"""
diff --git a/jupytext/languages.py b/jupytext/languages.py
index 86952749..4580cb63 100644
--- a/jupytext/languages.py
+++ b/jupytext/languages.py
@@ -33,7 +33,7 @@ _JUPYTER_LANGUAGES = _JUPYTER_LANGUAGES + [
 
 def default_language_from_metadata_and_ext(metadata, ext):
     """Return the default language given the notebook metadata, and a file extension"""
-    default_from_ext = _SCRIPT_EXTENSIONS.get(ext, {}).get('language', 'python')
+    default_from_ext = _SCRIPT_EXTENSIONS.get(ext, {}).get('language')
 
     language = (metadata.get('jupytext', {}).get('main_language')
                 or metadata.get('kernelspec', {}).get('language')
diff --git a/requirements-dev.txt b/requirements-dev.txt
index e22af845..c3ea37c3 100644
--- a/requirements-dev.txt
+++ b/requirements-dev.txt
@@ -4,3 +4,4 @@ pytest-cov==2.5.1
 codecov
 notebook
 jupyter_client
+nbconvert


Test patch:
diff --git a/tests/test_cli.py b/tests/test_cli.py
index c1c4e5dd..ce475b7b 100644
--- a/tests/test_cli.py
+++ b/tests/test_cli.py
@@ -432,9 +432,9 @@ def test_pre_commit_hook_sync_black_flake8(tmpdir, nb_file):
     with open(hook, 'w') as fp:
         fp.write('#!/bin/sh\n'
                  '# Pair ipynb notebooks to a python file, reformat content with black, and run flake8\n'
-                 '# Note: this hook only acts on ipynb files. When pulling, run jupytext --sync manually to '
+                 "# Note: this hook only acts on ipynb files. When pulling, run 'jupytext --sync' to "
                  'update the ipynb file.\n'
-                 'jupytext --pre-commit --from ipynb --set-formats ipynb,py --sync --pipe black --check flake8\n')
+                 'jupytext --pre-commit --from ipynb --set-formats ipynb,py --pipe black --check flake8\n')
 
     st = os.stat(hook)
     os.chmod(hook, st.st_mode | stat.S_IEXEC)
@@ -499,18 +499,8 @@ def test_set_formats(py_file, tmpdir):
 
     copyfile(py_file, tmp_py)
 
-    jupytext(['--to', 'ipynb', tmp_py, '--set-formats', 'ipynb,py:light'])
-
-    nb = read(tmp_ipynb)
-    assert nb.metadata['jupytext']['formats'] == 'ipynb,py:light'
-
-
-@pytest.mark.parametrize('py_file', list_notebooks('python'))
-def test_set_formats_py_file_only(py_file, tmpdir):
-    tmp_py = str(tmpdir.join('notebook.py'))
-    copyfile(py_file, tmp_py)
     jupytext([tmp_py, '--set-formats', 'ipynb,py:light'])
-    nb = read(tmp_py)
+    nb = read(tmp_ipynb)
     assert nb.metadata['jupytext']['formats'] == 'ipynb,py:light'
 
 
diff --git a/tests/test_execute.py b/tests/test_execute.py
new file mode 100644
index 00000000..625687a7
--- /dev/null
+++ b/tests/test_execute.py
@@ -0,0 +1,88 @@
+from .utils import requires_nbconvert, requires_ir_kernel
+from jupytext import read
+from jupytext.cli import jupytext
+
+
+@requires_nbconvert
+def test_pipe_nbconvert_execute(tmpdir):
+    tmp_ipynb = str(tmpdir.join('notebook.ipynb'))
+    tmp_py = str(tmpdir.join('notebook.py'))
+
+    with open(tmp_py, 'w') as fp:
+        fp.write("""1 + 2
+""")
+
+    jupytext(args=[tmp_py, '--to', 'ipynb', '--pipe-fmt', 'ipynb',
+                   '--pipe', 'jupyter nbconvert --stdin --stdout --to notebook --execute'])
+
+    nb = read(tmp_ipynb)
+    assert len(nb.cells) == 1
+    assert nb.cells[0].outputs[0]['data'] == {'text/plain': '3'}
+
+
+@requires_nbconvert
+def test_pipe_nbconvert_execute_sync(tmpdir):
+    tmp_ipynb = str(tmpdir.join('notebook.ipynb'))
+    tmp_py = str(tmpdir.join('notebook.py'))
+
+    with open(tmp_py, 'w') as fp:
+        fp.write("""1 + 2
+""")
+
+    jupytext(args=[tmp_py, '--set-formats', 'py,ipynb', '--sync', '--pipe-fmt', 'ipynb',
+                   '--pipe', 'jupyter nbconvert --stdin --stdout --to notebook --execute'])
+
+    nb = read(tmp_ipynb)
+    assert len(nb.cells) == 1
+    assert nb.cells[0].outputs[0]['data'] == {'text/plain': '3'}
+
+
+@requires_nbconvert
+def test_execute(tmpdir):
+    tmp_ipynb = str(tmpdir.join('notebook.ipynb'))
+    tmp_py = str(tmpdir.join('notebook.py'))
+
+    with open(tmp_py, 'w') as fp:
+        fp.write("""1 + 2
+""")
+
+    jupytext(args=[tmp_py, '--to', 'ipynb', '--execute'])
+
+    nb = read(tmp_ipynb)
+    assert len(nb.cells) == 1
+    assert nb.cells[0].outputs[0]['data'] == {'text/plain': '3'}
+
+
+@requires_nbconvert
+def test_execute_sync(tmpdir):
+    tmp_ipynb = str(tmpdir.join('notebook.ipynb'))
+    tmp_py = str(tmpdir.join('notebook.py'))
+
+    with open(tmp_py, 'w') as fp:
+        fp.write("""1 + 2
+""")
+
+    jupytext(args=[tmp_py, '--set-formats', 'py,ipynb', '--sync', '--execute'])
+
+    nb = read(tmp_ipynb)
+    assert len(nb.cells) == 1
+    assert nb.cells[0].outputs[0]['data'] == {'text/plain': '3'}
+
+
+@requires_nbconvert
+@requires_ir_kernel
+def test_execute_r(tmpdir):
+    tmp_ipynb = str(tmpdir.join('notebook.ipynb'))
+    tmp_md = str(tmpdir.join('notebook.md'))
+
+    with open(tmp_md, 'w') as fp:
+        fp.write("""```r
+1 + 2 + 3
+```
+""")
+
+    jupytext(args=[tmp_md, '--to', 'ipynb', '--execute'])
+
+    nb = read(tmp_ipynb)
+    assert len(nb.cells) == 1
+    assert nb.cells[0].outputs[0]['data']['text/markdown'] == '6'
diff --git a/tests/test_read_simple_markdown.py b/tests/test_read_simple_markdown.py
index 44d2793c..cd8f2700 100644
--- a/tests/test_read_simple_markdown.py
+++ b/tests/test_read_simple_markdown.py
@@ -66,11 +66,50 @@ cat(stringi::stri_rand_lipsum(3), sep='\n\n')
     compare(markdown, markdown2)
 
 
+def test_read_mostly_R_markdown_file(markdown="""```R
+ls()
+```
+
+```R
+cat(stringi::stri_rand_lipsum(3), sep='\n\n')
+```
+"""):
+    nb = jupytext.reads(markdown, 'md')
+    assert nb.metadata['jupytext']['main_language'] == 'R'
+    compare(nb.cells, [{'cell_type': 'code',
+                        'metadata': {},
+                        'execution_count': None,
+                        'source': 'ls()',
+                        'outputs': []},
+                       {'cell_type': 'code',
+                        'metadata': {},
+                        'execution_count': None,
+                        'source': "cat(stringi::stri_rand_lipsum(3), sep='\n\n')",
+                        'outputs': []}])
+
+    markdown2 = jupytext.writes(nb, 'md')
+    compare(markdown, markdown2)
+
+
+def test_read_markdown_file_no_language(markdown="""```
+ls
+```
+
+```
+echo 'Hello World'
+```
+"""):
+    nb = jupytext.reads(markdown, 'md')
+    markdown2 = jupytext.writes(nb, 'md')
+    compare(markdown, markdown2)
+
+
 def test_read_julia_notebook(markdown="""```julia
 1 + 1
 ```
 """):
     nb = jupytext.reads(markdown, 'md')
+    assert nb.metadata['jupytext']['main_language'] == 'julia'
     assert len(nb.cells) == 1
     assert nb.cells[0].cell_type == 'code'
     markdown2 = jupytext.writes(nb, 'md')
diff --git a/tests/utils.py b/tests/utils.py
index 896042e8..b0d2335b 100644
--- a/tests/utils.py
+++ b/tests/utils.py
@@ -5,6 +5,7 @@ import pytest
 from jupytext.cli import system
 from jupytext.cell_reader import rst2md
 from jupytext.pandoc import is_pandoc_available
+from jupytext.kernels import kernelspec_from_language
 
 skip_if_dict_is_not_ordered = pytest.mark.skipif(
     sys.version_info < (3, 6),
@@ -13,7 +14,9 @@ skip_if_dict_is_not_ordered = pytest.mark.skipif(
 
 def tool_version(tool):
     try:
-        return system(tool, '--version')
+        args = tool.split(' ')
+        args.append('--version')
+        return system(*args)
     except (OSError, SystemExit):  # pragma: no cover
         return None
 
@@ -22,8 +25,10 @@ requires_jupytext_installed = pytest.mark.skipif(not tool_version('jupytext'), r
 requires_black = pytest.mark.skipif(not tool_version('black'), reason='black not found')
 requires_flake8 = pytest.mark.skipif(not tool_version('flake8'), reason='flake8 not found')
 requires_autopep8 = pytest.mark.skipif(not tool_version('autopep8'), reason='autopep8 not found')
+requires_nbconvert = pytest.mark.skipif(not tool_version('jupyter nbconvert'), reason='nbconvert not found')
 requires_sphinx_gallery = pytest.mark.skipif(not rst2md, reason='sphinx_gallery is not available')
 requires_pandoc = pytest.mark.skipif(not is_pandoc_available(), reason='pandoc>=2.7.2 is not available')
+requires_ir_kernel = pytest.mark.skipif(kernelspec_from_language('R') is None, reason='irkernel is not installed')
 
 
 def list_notebooks(path='ipynb', skip='World'):