Repository: mwouts/jupytext
Pull request #128 (2018-11-12 23:07:00)

Version 0.8.5
**Improvements**

- `bash` scripts as notebooks (#127)
- R scripts with `.r` extension are supported (#122)
- Jupytext selects the first kernel that matches the language (#120)

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

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

```diff
@@            Coverage Diff             @@
##           master     #128      +/-   ##
==========================================
- Coverage   97.16%   90.48%   -6.68%     
==========================================
  Files          52       52              
  Lines        3737     3784      +47     
==========================================
- Hits         3631     3424     -207     
- Misses        106      360     +254
```


| [Impacted Files](https://codecov.io/gh/mwouts/jupytext/pull/128?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [tests/test\_escape\_magics.py](https://codecov.io/gh/mwouts/jupytext/pull/128/diff?src=pr&el=tree#diff-dGVzdHMvdGVzdF9lc2NhcGVfbWFnaWNzLnB5) | `100% <ø> (ø)` | :arrow_up: |
| [jupytext/languages.py](https://codecov.io/gh/mwouts/jupytext/pull/128/diff?src=pr&el=tree#diff-anVweXRleHQvbGFuZ3VhZ2VzLnB5) | `100% <ø> (ø)` | :arrow_up: |
| [jupytext/formats.py](https://codecov.io/gh/mwouts/jupytext/pull/128/diff?src=pr&el=tree#diff-anVweXRleHQvZm9ybWF0cy5weQ==) | `96.89% <100%> (ø)` | :arrow_up: |
| [tests/test\_read\_simple\_python.py](https://codecov.io/gh/mwouts/jupytext/pull/128/diff?src=pr&el=tree#diff-dGVzdHMvdGVzdF9yZWFkX3NpbXBsZV9weXRob24ucHk=) | `100% <100%> (ø)` | :arrow_up: |
| [jupytext/cell\_reader.py](https://codecov.io/gh/mwouts/jupytext/pull/128/diff?src=pr&el=tree#diff-anVweXRleHQvY2VsbF9yZWFkZXIucHk=) | `95.23% <100%> (-1.86%)` | :arrow_down: |
| [jupytext/version.py](https://codecov.io/gh/mwouts/jupytext/pull/128/diff?src=pr&el=tree#diff-anVweXRleHQvdmVyc2lvbi5weQ==) | `100% <100%> (ø)` | :arrow_up: |
| [jupytext/cell\_metadata.py](https://codecov.io/gh/mwouts/jupytext/pull/128/diff?src=pr&el=tree#diff-anVweXRleHQvY2VsbF9tZXRhZGF0YS5weQ==) | `99.1% <100%> (-0.9%)` | :arrow_down: |
| [jupytext/combine.py](https://codecov.io/gh/mwouts/jupytext/pull/128/diff?src=pr&el=tree#diff-anVweXRleHQvY29tYmluZS5weQ==) | `95.91% <100%> (-4.09%)` | :arrow_down: |
| [tests/test\_read\_simple\_R.py](https://codecov.io/gh/mwouts/jupytext/pull/128/diff?src=pr&el=tree#diff-dGVzdHMvdGVzdF9yZWFkX3NpbXBsZV9SLnB5) | `100% <100%> (ø)` | :arrow_up: |
| [tests/test\_ipynb\_to\_R.py](https://codecov.io/gh/mwouts/jupytext/pull/128/diff?src=pr&el=tree#diff-dGVzdHMvdGVzdF9pcHluYl90b19SLnB5) | `100% <100%> (ø)` | :arrow_up: |
| ... and [18 more](https://codecov.io/gh/mwouts/jupytext/pull/128/diff?src=pr&el=tree-more) | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/mwouts/jupytext/pull/128?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/128?src=pr&el=footer). Last update [724a60f...80d230d](https://codecov.io/gh/mwouts/jupytext/pull/128?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 cfa724c6..db95f0b2 100644
--- a/HISTORY.rst
+++ b/HISTORY.rst
@@ -3,6 +3,15 @@
 Release History
 ---------------
 
+0.8.5 (2018-11-13)
+++++++++++++++++++++++
+
+**Improvements**
+
+- ``bash`` scripts as notebooks (#127)
+- R scripts with ``.r`` extension are supported (#122)
+- Jupytext selects the first kernel that matches the language (#120)
+
 0.8.4 (2018-10-29)
 ++++++++++++++++++++++
 
diff --git a/jupytext/cell_metadata.py b/jupytext/cell_metadata.py
index adfe9342..835c07e1 100644
--- a/jupytext/cell_metadata.py
+++ b/jupytext/cell_metadata.py
@@ -26,14 +26,16 @@ except NameError:
 
 _BOOLEAN_OPTIONS_DICTIONARY = [('hide_input', 'echo', True),
                                ('hide_output', 'include', True)]
-_IGNORE_CELL_METADATA = ','.join('-{}'.format(name) for name in [
-    # Frequent cell metadata that should not enter the text representation
-    # (these metadata are preserved in the paired Jupyter notebook).
-    'autoscroll', 'collapsed', 'scrolled', 'trusted', 'ExecuteTime',
+_JUPYTEXT_CELL_METADATA = [
     # Pre-jupytext metadata
     'skipline', 'noskipline',
     # Jupytext metadata
-    'cell_marker', 'lines_to_next_cell', 'lines_to_end_of_cell_marker'])
+    'cell_marker', 'lines_to_next_cell', 'lines_to_end_of_cell_marker']
+_IGNORE_CELL_METADATA = ','.join('-{}'.format(name) for name in [
+    # Frequent cell metadata that should not enter the text representation
+    # (these metadata are preserved in the paired Jupyter notebook).
+    'autoscroll', 'collapsed', 'scrolled', 'trusted', 'ExecuteTime'] +
+                                 _JUPYTEXT_CELL_METADATA)
 _PERCENT_CELL = re.compile(
     r'(# |#)%%([^\{\[]*)(|\[raw\]|\[markdown\])([^\{\[]*)(|\{.*\})\s*$')
 
diff --git a/jupytext/cell_reader.py b/jupytext/cell_reader.py
index b2da8c15..b8d92dce 100644
--- a/jupytext/cell_reader.py
+++ b/jupytext/cell_reader.py
@@ -203,7 +203,7 @@ class BaseCellReader(object):
 
         if not is_active(self.ext, self.metadata) or \
                 ('active' not in self.metadata and self.language and self.language != self.default_language):
-            self.content = uncomment(source, self.comment if self.ext != '.R' else '#')
+            self.content = uncomment(source, self.comment if self.ext not in ['.r', '.R'] else '#')
         else:
             self.content = self.uncomment_code_and_magics(source)
 
diff --git a/jupytext/combine.py b/jupytext/combine.py
index 84cb5654..f49291ab 100644
--- a/jupytext/combine.py
+++ b/jupytext/combine.py
@@ -2,7 +2,7 @@
 """
 import re
 from copy import copy
-from .cell_metadata import _IGNORE_CELL_METADATA
+from .cell_metadata import _IGNORE_CELL_METADATA, _JUPYTEXT_CELL_METADATA
 from .header import _DEFAULT_NOTEBOOK_METADATA
 from .metadata_filter import filter_metadata
 
@@ -58,7 +58,7 @@ def combine_inputs_with_outputs(nb_source, nb_outputs):
                                         _IGNORE_CELL_METADATA)
 
                     for key in ocell.metadata:
-                        if key not in ocell_filtered_metadata:
+                        if key not in ocell_filtered_metadata and key not in _JUPYTEXT_CELL_METADATA:
                             cell.metadata[key] = ocell.metadata[key]
 
                     output_code_cells = output_code_cells[(i + 1):]
diff --git a/jupytext/contentsmanager.py b/jupytext/contentsmanager.py
index 58163776..b5302146 100644
--- a/jupytext/contentsmanager.py
+++ b/jupytext/contentsmanager.py
@@ -15,6 +15,7 @@ except ImportError:
     pass
 
 from notebook.services.contents.filemanager import FileContentsManager
+from jupyter_client.kernelspec import find_kernel_specs, get_kernel_spec
 
 import jupytext
 from .combine import combine_inputs_with_outputs
@@ -23,6 +24,18 @@ from .formats import check_file_version, NOTEBOOK_EXTENSIONS, \
 from .metadata_filter import metadata_filter_as_dict
 
 
+def kernelspec_from_language(language):
+    """Return the kernel specification for the first kernel with a matching language"""
+    try:
+        for name in find_kernel_specs():
+            ks = get_kernel_spec(name)
+            if ks.language == language:
+                return {'name': name, 'language': language, 'display_name': ks.display_name}
+    except (KeyError, ValueError):
+        pass
+    return None
+
+
 def _jupytext_writes(ext, format_name):
     def _writes(nbk, version=nbformat.NO_CONVERT, **kwargs):
         return jupytext.writes(nbk, version=version, ext=ext, format_name=format_name, **kwargs)
@@ -341,7 +354,7 @@ class TextFileContentsManager(FileContentsManager, Configurable):
             except OverflowError:
                 pass
 
-            jupytext_metadata = model['content']['metadata'].setdefault('jupytext', {})
+            jupytext_metadata = model['content']['metadata'].get('jupytext', {})
             if self.default_notebook_metadata_filter:
                 (jupytext_metadata.setdefault('metadata_filter', {})
                  .setdefault('notebook', self.default_notebook_metadata_filter))
@@ -354,11 +367,21 @@ class TextFileContentsManager(FileContentsManager, Configurable):
                 if filter is not None:
                     jupytext_metadata['metadata_filter'][filter_level] = metadata_filter_as_dict(filter)
 
+            if jupytext_metadata:
+                model['content']['metadata']['jupytext'] = jupytext_metadata
+
             if model_outputs:
                 combine_inputs_with_outputs(model['content'], model_outputs['content'])
             elif not fmt.endswith('.ipynb'):
-                self.notary.sign(model['content'])
-                self.mark_trusted_cells(model['content'], path)
+                nb = model['content']
+                language = nb.metadata.get('jupytext', {}).get('main_language', 'python')
+                if 'kernelspec' not in nb.metadata and language != 'python':
+                    kernelspec = kernelspec_from_language(language)
+                    if kernelspec:
+                        nb.metadata['kernelspec'] = kernelspec
+
+                self.notary.sign(nb)
+                self.mark_trusted_cells(nb, path)
 
             return model
 
diff --git a/jupytext/formats.py b/jupytext/formats.py
index 9da337da..c3f6d65f 100644
--- a/jupytext/formats.py
+++ b/jupytext/formats.py
@@ -54,16 +54,16 @@ JUPYTEXT_FORMATS = \
             cell_reader_class=RMarkdownCellReader,
             cell_exporter_class=RMarkdownCellExporter,
             # Version 1.0 on 2018-08-22 - jupytext v0.5.2 : Initial version
-            current_version_number='1.0'),
-
+            current_version_number='1.0')] + \
+    [
         NotebookFormatDescription(
             format_name='spin',
-            extension='.R',
+            extension=ext,
             header_prefix="#'",
             cell_reader_class=RScriptCellReader,
             cell_exporter_class=RScriptCellExporter,
             # Version 1.0 on 2018-08-22 - jupytext v0.5.2 : Initial version
-            current_version_number='1.0')] + \
+            current_version_number='1.0') for ext in ['.r', '.R']] + \
     [
         NotebookFormatDescription(
             format_name='light',
@@ -148,7 +148,7 @@ def read_metadata(text, ext):
         comment = _SCRIPT_EXTENSIONS.get(ext, {}).get('comment', '#')
 
     metadata, _, _ = header_to_metadata_and_cell(lines, comment)
-    if ext == '.R' and not metadata:
+    if ext in ['.r', '.R'] and not metadata:
         metadata, _, _ = header_to_metadata_and_cell(lines, "#'")
 
     return metadata
@@ -182,7 +182,7 @@ def guess_format(text, ext):
         twenty_hash_count = 0
         double_percent_count = 0
 
-        parser = StringParser(language='R' if ext == '.R' else 'python')
+        parser = StringParser(language='R' if ext in ['.r', '.R'] else 'python')
         for line in lines:
             parser.read_line(line)
             if parser.is_quoted():
@@ -190,7 +190,8 @@ def guess_format(text, ext):
 
             # Don't count escaped Jupyter magics (no space between
             # %% and command) as cells
-            if double_percent_re.match(line) or double_percent_and_space_re.match(line) or nbconvert_script_re.match(line):
+            if double_percent_re.match(line) or double_percent_and_space_re.match(line) or \
+                    nbconvert_script_re.match(line):
                 double_percent_count += 1
 
             if line.startswith(twenty_hash) and ext == '.py':
diff --git a/jupytext/jupytext.py b/jupytext/jupytext.py
index 8ff52377..34a8fe2d 100644
--- a/jupytext/jupytext.py
+++ b/jupytext/jupytext.py
@@ -59,10 +59,9 @@ class TextNotebookReader(NotebookReader):
             lines = lines[pos:]
 
         if self.freeze_metadata:
-            metadata['jupytext'] = {'metadata_filter': {
-                'notebook': {'additional': list(metadata.keys()), 'excluded': 'all'}}}
-            metadata['jupytext']['metadata_filter']['cells'] = {
-                'additional': list(cell_metadata), 'excluded': 'all'}
+            metadata.setdefault('jupytext', {})['metadata_filter'] = {
+                'notebook': {'additional': list(metadata.keys()), 'excluded': 'all'},
+                'cells': {'additional': list(cell_metadata), 'excluded': 'all'}}
 
         set_main_and_cell_language(metadata, cells, self.format.extension)
 
diff --git a/jupytext/languages.py b/jupytext/languages.py
index 8da4b470..8779520c 100644
--- a/jupytext/languages.py
+++ b/jupytext/languages.py
@@ -7,9 +7,11 @@ _JUPYTER_LANGUAGES_RE = [re.compile(r"^%%{}\s*".format(lang)) for lang in _JUPYT
 
 _SCRIPT_EXTENSIONS = {'.py': {'language': 'python', 'comment': '#'},
                       '.R': {'language': 'R', 'comment': '#'},
+                      '.r': {'language': 'R', 'comment': '#'},
                       '.jl': {'language': 'julia', 'comment': '#'},
                       '.cpp': {'language': 'c++', 'comment': '//'},
-                      '.ss': {'language': 'scheme', 'comment': ';;'}}
+                      '.ss': {'language': 'scheme', 'comment': ';;'},
+                      '.sh': {'language': 'bash', 'comment': '#'}}
 
 
 def default_language_from_metadata_and_ext(notebook, ext):
diff --git a/jupytext/version.py b/jupytext/version.py
index 92fc5b6e..4dcf642b 100644
--- a/jupytext/version.py
+++ b/jupytext/version.py
@@ -1,3 +1,3 @@
 """Jupytext's version number"""
 
-__version__ = '0.8.4'
+__version__ = '0.8.5'


Test patch:
diff --git a/tests/notebooks/ipynb_bash/sample_bash_notebook.ipynb b/tests/notebooks/ipynb_bash/sample_bash_notebook.ipynb
new file mode 100644
index 00000000..c9d0bd24
--- /dev/null
+++ b/tests/notebooks/ipynb_bash/sample_bash_notebook.ipynb
@@ -0,0 +1,70 @@
+{
+ "cells": [
+  {
+   "cell_type": "code",
+   "execution_count": 1,
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      " \u001b[0m\u001b[01;32mREADME.md\u001b[0m  \u001b[01;32m'Sample bash notebook.ipynb'\u001b[0m   \u001b[34;42mbinder\u001b[0m   \u001b[34;42mdemo\u001b[0m   \u001b[34;42mnotebook\u001b[0m   \u001b[34;42mslides\u001b[0m\n"
+     ]
+    }
+   ],
+   "source": [
+    "ls"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 2,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# https://coderwall.com/p/euwpig/a-better-git-log\n",
+    "git config --global alias.lg \"log --color --graph --pretty=format:'%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%cr) %C(bold blue)<%an>%Creset' --abbrev-commit\""
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 3,
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "* \u001b[31m64accf1\u001b[m -\u001b[33m (HEAD -> master, origin/master, origin/HEAD)\u001b[m Slides reviewed \u001b[32m(23 hours ago) \u001b[1;34m<Marc Wouts>\u001b[m\n",
+      "* \u001b[31mbf3d0ab\u001b[m -\u001b[33m\u001b[m README \u001b[32m(23 hours ago) \u001b[1;34m<Marc Wouts>\u001b[m\n",
+      "* \u001b[31md1116f6\u001b[m -\u001b[33m\u001b[m Use Binder \u001b[32m(23 hours ago) \u001b[1;34m<Marc Wouts>\u001b[m\n",
+      "* \u001b[31m3526fc6\u001b[m -\u001b[33m\u001b[m Demo notebook has data in the metric explorer \u001b[32m(23 hours ago) \u001b[1;34m<Marc Wouts>\u001b[m\n",
+      "* \u001b[31mf4139fd\u001b[m -\u001b[33m\u001b[m Ignore more patterns \u001b[32m(23 hours ago) \u001b[1;34m<Marc Wouts>\u001b[m\n",
+      "* \u001b[31m984ccd1\u001b[m -\u001b[33m\u001b[m Demo script updated \u001b[32m(2 days ago) \u001b[1;34m<Marc Wouts>\u001b[m\n",
+      "* \u001b[31m6874a22\u001b[m -\u001b[33m\u001b[m Updated final notebook \u001b[32m(2 days ago) \u001b[1;34m<Marc Wouts>\u001b[m\n",
+      "* \u001b[31ma6b8f88\u001b[m -\u001b[33m\u001b[m Slides and demo are getting ready \u001b[32m(4 days ago) \u001b[1;34m<Marc Wouts>\u001b[m"
+     ]
+    }
+   ],
+   "source": [
+    "git lg"
+   ]
+  }
+ ],
+ "metadata": {
+  "kernelspec": {
+   "display_name": "Bash",
+   "language": "bash",
+   "name": "bash"
+  },
+  "language_info": {
+   "codemirror_mode": "shell",
+   "file_extension": ".sh",
+   "mimetype": "text/x-sh",
+   "name": "bash"
+  }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 2
+}
diff --git a/tests/notebooks/mirror/ipynb_to_percent/ir_notebook.low.r b/tests/notebooks/mirror/ipynb_to_percent/ir_notebook.low.r
new file mode 100644
index 00000000..e8ec06ca
--- /dev/null
+++ b/tests/notebooks/mirror/ipynb_to_percent/ir_notebook.low.r
@@ -0,0 +1,26 @@
+# ---
+# jupyter:
+#   kernelspec:
+#     display_name: R
+#     language: R
+#     name: ir
+#   language_info:
+#     codemirror_mode: r
+#     file_extension: .r
+#     mimetype: text/x-r-source
+#     name: R
+#     pygments_lexer: r
+#     version: 3.5.0
+# ---
+
+# %% [markdown]
+# This is a jupyter notebook that uses the IR kernel.
+
+# %%
+sum(1:10)
+
+# %%
+plot(cars)
+
+# %%
+
diff --git a/tests/notebooks/mirror/ipynb_to_percent/sample_bash_notebook.sh b/tests/notebooks/mirror/ipynb_to_percent/sample_bash_notebook.sh
new file mode 100644
index 00000000..ddd3275f
--- /dev/null
+++ b/tests/notebooks/mirror/ipynb_to_percent/sample_bash_notebook.sh
@@ -0,0 +1,22 @@
+# ---
+# jupyter:
+#   kernelspec:
+#     display_name: Bash
+#     language: bash
+#     name: bash
+#   language_info:
+#     codemirror_mode: shell
+#     file_extension: .sh
+#     mimetype: text/x-sh
+#     name: bash
+# ---
+
+# %%
+ls
+
+# %%
+# https://coderwall.com/p/euwpig/a-better-git-log
+git config --global alias.lg "log --color --graph --pretty=format:'%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%cr) %C(bold blue)<%an>%Creset' --abbrev-commit"
+
+# %%
+git lg
diff --git a/tests/notebooks/mirror/ipynb_to_script/ir_notebook.low.r b/tests/notebooks/mirror/ipynb_to_script/ir_notebook.low.r
new file mode 100644
index 00000000..642d0bf7
--- /dev/null
+++ b/tests/notebooks/mirror/ipynb_to_script/ir_notebook.low.r
@@ -0,0 +1,22 @@
+#' ---
+#' jupyter:
+#'   kernelspec:
+#'     display_name: R
+#'     language: R
+#'     name: ir
+#'   language_info:
+#'     codemirror_mode: r
+#'     file_extension: .r
+#'     mimetype: text/x-r-source
+#'     name: R
+#'     pygments_lexer: r
+#'     version: 3.5.0
+#' ---
+
+#' This is a jupyter notebook that uses the IR kernel.
+
+sum(1:10)
+
+plot(cars)
+
+
diff --git a/tests/notebooks/mirror/ipynb_to_script/sample_bash_notebook.sh b/tests/notebooks/mirror/ipynb_to_script/sample_bash_notebook.sh
new file mode 100644
index 00000000..34250cd8
--- /dev/null
+++ b/tests/notebooks/mirror/ipynb_to_script/sample_bash_notebook.sh
@@ -0,0 +1,19 @@
+# ---
+# jupyter:
+#   kernelspec:
+#     display_name: Bash
+#     language: bash
+#     name: bash
+#   language_info:
+#     codemirror_mode: shell
+#     file_extension: .sh
+#     mimetype: text/x-sh
+#     name: bash
+# ---
+
+ls
+
+# https://coderwall.com/p/euwpig/a-better-git-log
+git config --global alias.lg "log --color --graph --pretty=format:'%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%cr) %C(bold blue)<%an>%Creset' --abbrev-commit"
+
+git lg
diff --git a/tests/notebooks/mirror/ipynb_to_spin/ir_notebook.R b/tests/notebooks/mirror/ipynb_to_spin/ir_notebook.R
new file mode 100644
index 00000000..642d0bf7
--- /dev/null
+++ b/tests/notebooks/mirror/ipynb_to_spin/ir_notebook.R
@@ -0,0 +1,22 @@
+#' ---
+#' jupyter:
+#'   kernelspec:
+#'     display_name: R
+#'     language: R
+#'     name: ir
+#'   language_info:
+#'     codemirror_mode: r
+#'     file_extension: .r
+#'     mimetype: text/x-r-source
+#'     name: R
+#'     pygments_lexer: r
+#'     version: 3.5.0
+#' ---
+
+#' This is a jupyter notebook that uses the IR kernel.
+
+sum(1:10)
+
+plot(cars)
+
+
diff --git a/tests/notebooks/mirror/ipynb_to_spin/ir_notebook.low.r b/tests/notebooks/mirror/ipynb_to_spin/ir_notebook.low.r
new file mode 100644
index 00000000..642d0bf7
--- /dev/null
+++ b/tests/notebooks/mirror/ipynb_to_spin/ir_notebook.low.r
@@ -0,0 +1,22 @@
+#' ---
+#' jupyter:
+#'   kernelspec:
+#'     display_name: R
+#'     language: R
+#'     name: ir
+#'   language_info:
+#'     codemirror_mode: r
+#'     file_extension: .r
+#'     mimetype: text/x-r-source
+#'     name: R
+#'     pygments_lexer: r
+#'     version: 3.5.0
+#' ---
+
+#' This is a jupyter notebook that uses the IR kernel.
+
+sum(1:10)
+
+plot(cars)
+
+
diff --git a/tests/test_escape_magics.py b/tests/test_escape_magics.py
index 30e2a952..f46ff481 100644
--- a/tests/test_escape_magics.py
+++ b/tests/test_escape_magics.py
@@ -57,7 +57,8 @@ def test_magics_are_commented(ext_and_format_name):
     ext, format_name = parse_one_format(ext_and_format_name)
     nb = new_notebook(cells=[new_code_cell('%pylab inline')],
                       metadata={'jupytext': {'comment_magics': True,
-                                             'main_language': 'R' if ext == '.R' else 'scheme' if ext == '.ss' else 'python'}})
+                                             'main_language': 'R' if ext in ['.r', '.R']
+                                             else 'scheme' if ext == '.ss' else 'python'}})
 
     text = jupytext.writes(nb, ext, format_name)
     assert '%pylab inline' not in text.splitlines()
@@ -75,7 +76,8 @@ def test_magics_are_not_commented(ext_and_format_name):
     ext, format_name = parse_one_format(ext_and_format_name)
     nb = new_notebook(cells=[new_code_cell('%pylab inline')],
                       metadata={'jupytext': {'comment_magics': False,
-                                             'main_language': 'R' if ext == '.R' else 'scheme' if ext == '.ss' else 'python'}})
+                                             'main_language': 'R' if ext in ['.r', '.R']
+                                             else 'scheme' if ext == '.ss' else 'python'}})
 
     text = jupytext.writes(nb, ext, format_name)
     assert '%pylab inline' in text.splitlines()
diff --git a/tests/test_ipynb_to_R.py b/tests/test_ipynb_to_R.py
index b1a1146f..12e62413 100644
--- a/tests/test_ipynb_to_R.py
+++ b/tests/test_ipynb_to_R.py
@@ -1,4 +1,5 @@
 import nbformat
+import itertools
 import pytest
 import jupytext
 from jupytext.compare import compare_notebooks
@@ -7,8 +8,8 @@ from .utils import list_notebooks
 jupytext.header.INSERT_AND_CHECK_VERSION_NUMBER = False
 
 
-@pytest.mark.parametrize('nb_file', list_notebooks('ipynb_R'))
-def test_identity_source_write_read(nb_file):
+@pytest.mark.parametrize('nb_file,ext', itertools.product(list_notebooks('ipynb_R'), ['.r', '.R']))
+def test_identity_source_write_read(nb_file, ext):
     """
     Test that writing the notebook with R, and read again,
     is the same as removing outputs
@@ -17,7 +18,7 @@ def test_identity_source_write_read(nb_file):
     with open(nb_file) as fp:
         nb1 = nbformat.read(fp, as_version=4)
 
-    R = jupytext.writes(nb1, ext='.R')
-    nb2 = jupytext.reads(R, ext='.R')
+    R = jupytext.writes(nb1, ext=ext)
+    nb2 = jupytext.reads(R, ext=ext)
 
     compare_notebooks(nb1, nb2)
diff --git a/tests/test_mirror.py b/tests/test_mirror.py
index 6693406e..49f4e424 100644
--- a/tests/test_mirror.py
+++ b/tests/test_mirror.py
@@ -98,11 +98,21 @@ def test_ipynb_to_R(nb_file):
     assert_conversion_same_as_mirror(nb_file, '.R', 'ipynb_to_script')
 
 
+@pytest.mark.parametrize('nb_file', list_notebooks('ipynb_R'))
+def test_ipynb_to_r(nb_file):
+    assert_conversion_same_as_mirror(nb_file, '.low.r', 'ipynb_to_script')
+
+
 @pytest.mark.parametrize('nb_file', list_notebooks('ipynb_scheme'))
 def test_ipynb_to_scheme(nb_file):
     assert_conversion_same_as_mirror(nb_file, '.ss', 'ipynb_to_script')
 
 
+@pytest.mark.parametrize('nb_file', list_notebooks('ipynb_bash'))
+def test_ipynb_to_bash(nb_file):
+    assert_conversion_same_as_mirror(nb_file, '.sh', 'ipynb_to_script')
+
+
 @pytest.mark.parametrize('nb_file', list_notebooks('ipynb_cpp'))
 def test_ipynb_to_cpp(nb_file):
     assert_conversion_same_as_mirror(nb_file, '.cpp', 'ipynb_to_script')
@@ -123,6 +133,21 @@ def test_ipynb_to_R_percent(nb_file):
     assert_conversion_same_as_mirror(nb_file, '.R', 'ipynb_to_percent', format_name='percent')
 
 
+@pytest.mark.parametrize('nb_file', list_notebooks('ipynb_R'))
+def test_ipynb_to_r_percent(nb_file):
+    assert_conversion_same_as_mirror(nb_file, '.low.r', 'ipynb_to_percent', format_name='percent')
+
+
+@pytest.mark.parametrize('nb_file', list_notebooks('ipynb_R'))
+def test_ipynb_to_R_spin(nb_file):
+    assert_conversion_same_as_mirror(nb_file, '.R', 'ipynb_to_spin', format_name='spin')
+
+
+@pytest.mark.parametrize('nb_file', list_notebooks('ipynb_R'))
+def test_ipynb_to_r_spin(nb_file):
+    assert_conversion_same_as_mirror(nb_file, '.low.r', 'ipynb_to_spin', format_name='spin')
+
+
 @pytest.mark.parametrize('nb_file', list_notebooks('ipynb_cpp'))
 def test_ipynb_to_cpp_percent(nb_file):
     assert_conversion_same_as_mirror(nb_file, '.cpp', 'ipynb_to_percent', format_name='percent')
@@ -133,6 +158,11 @@ def test_ipynb_to_scheme_percent(nb_file):
     assert_conversion_same_as_mirror(nb_file, '.ss', 'ipynb_to_percent', format_name='percent')
 
 
+@pytest.mark.parametrize('nb_file', list_notebooks('ipynb_bash'))
+def test_ipynb_to_bash_percent(nb_file):
+    assert_conversion_same_as_mirror(nb_file, '.sh', 'ipynb_to_percent', format_name='percent')
+
+
 @pytest.mark.parametrize('nb_file', list_notebooks('percent'))
 def test_percent_to_ipynb(nb_file):
     assert_conversion_same_as_mirror(nb_file, '.ipynb', 'script_to_ipynb', format_name='percent')
diff --git a/tests/test_read_simple_R.py b/tests/test_read_simple_R.py
index 8c49994b..c0561457 100644
--- a/tests/test_read_simple_R.py
+++ b/tests/test_read_simple_R.py
@@ -1,12 +1,14 @@
 # -*- coding: utf-8 -*-
 
+import pytest
 from testfixtures import compare
 import jupytext
 
 jupytext.header.INSERT_AND_CHECK_VERSION_NUMBER = False
 
 
-def test_read_simple_file(rnb="""#' ---
+@pytest.mark.parametrize('ext', ['.r', '.R'])
+def test_read_simple_file(ext, rnb="""#' ---
 #' title: Simple file
 #' ---
 
@@ -21,7 +23,7 @@ f <- function(x) {
 h <- function(y)
     y + 1
 """):
-    nb = jupytext.reads(rnb, ext='.R')
+    nb = jupytext.reads(rnb, ext=ext)
     assert len(nb.cells) == 4
     assert nb.cells[0].cell_type == 'raw'
     assert nb.cells[0].source == '---\ntitle: Simple file\n---'
@@ -36,11 +38,12 @@ h <- function(y)
     compare(nb.cells[3].source, '''h <- function(y)
     y + 1''')
 
-    rnb2 = jupytext.writes(nb, ext='.R')
+    rnb2 = jupytext.writes(nb, ext=ext)
     compare(rnb, rnb2)
 
 
-def test_read_less_simple_file(rnb="""#' ---
+@pytest.mark.parametrize('ext', ['.r', '.R'])
+def test_read_less_simple_file(ext, rnb="""#' ---
 #' title: Less simple file
 #' ---
 
@@ -58,7 +61,7 @@ h <- function(y) {
     return(y-1)
 }
 """):
-    nb = jupytext.reads(rnb, ext='.R')
+    nb = jupytext.reads(rnb, ext=ext)
 
     assert len(nb.cells) == 4
     assert nb.cells[0].cell_type == 'raw'
@@ -79,11 +82,12 @@ h <- function(y) {
     return(y-1)
 }''')
 
-    rnb2 = jupytext.writes(nb, ext='.R')
+    rnb2 = jupytext.writes(nb, ext=ext)
     compare(rnb, rnb2)
 
 
-def test_no_space_after_code(rnb=u"""# -*- coding: utf-8 -*-
+@pytest.mark.parametrize('ext', ['.r', '.R'])
+def test_no_space_after_code(ext, rnb=u"""# -*- coding: utf-8 -*-
 #' Markdown cell
 
 f <- function(x)
@@ -93,7 +97,7 @@ f <- function(x)
 
 #' And a new cell, and non ascii contênt
 """):
-    nb = jupytext.reads(rnb, ext='.R')
+    nb = jupytext.reads(rnb, ext=ext)
 
     assert len(nb.cells) == 3
     assert nb.cells[0].cell_type == 'markdown'
@@ -106,20 +110,22 @@ f <- function(x)
     assert nb.cells[2].cell_type == 'markdown'
     assert nb.cells[2].source == u'And a new cell, and non ascii contênt'
 
-    rnb2 = jupytext.writes(nb, ext='.R')
+    rnb2 = jupytext.writes(nb, ext=ext)
     compare(rnb, rnb2)
 
 
-def test_read_write_script(rnb="""#!/usr/bin/env Rscript
+@pytest.mark.parametrize('ext', ['.r', '.R'])
+def test_read_write_script(ext, rnb="""#!/usr/bin/env Rscript
 # coding=utf-8
 print('Hello world')
 """):
-    nb = jupytext.reads(rnb, ext='.R')
-    rnb2 = jupytext.writes(nb, ext='.R')
+    nb = jupytext.reads(rnb, ext=ext)
+    rnb2 = jupytext.writes(nb, ext=ext)
     compare(rnb, rnb2)
 
 
-def test_escape_start_pattern(rnb="""#' The code start pattern '#+' can
+@pytest.mark.parametrize('ext', ['.r', '.R'])
+def test_escape_start_pattern(ext, rnb="""#' The code start pattern '#+' can
 #' appear in code and markdown cells.
 
 #' In markdown cells it is escaped like here:
@@ -129,7 +135,7 @@ def test_escape_start_pattern(rnb="""#' The code start pattern '#+' can
 # #+ cell_name language="python"
 1 + 1
 """):
-    nb = jupytext.reads(rnb, ext='.R')
+    nb = jupytext.reads(rnb, ext=ext)
     assert len(nb.cells) == 3
     assert nb.cells[0].cell_type == 'markdown'
     assert nb.cells[1].cell_type == 'markdown'
@@ -140,5 +146,5 @@ def test_escape_start_pattern(rnb="""#' The code start pattern '#+' can
             '''# In code cells like this one, it is also escaped
 #+ cell_name language="python"
 1 + 1''')
-    rnb2 = jupytext.writes(nb, ext='.R')
+    rnb2 = jupytext.writes(nb, ext=ext)
     compare(rnb, rnb2)
diff --git a/tests/test_read_simple_python.py b/tests/test_read_simple_python.py
index 52f1bf58..dacc8222 100644
--- a/tests/test_read_simple_python.py
+++ b/tests/test_read_simple_python.py
@@ -499,6 +499,40 @@ d = 6
     compare(script, script2)
 
 
+def test_notebook_two_blank_lines_before_next_cell(script="""# +
+# This is cell with a function
+
+def f(x):
+    return 4
+
+
+# +
+# Another cell
+c = 5
+
+
+def g(x):
+    return 6
+
+
+# +
+# Final cell
+
+1 + 1
+"""):
+    notebook = jupytext.reads(script, ext='.py')
+    assert len(notebook.cells) == 3
+    for cell in notebook.cells:
+        lines = cell.source.splitlines()
+        if len(lines) != 1:
+            assert lines[0]
+            assert lines[-1]
+
+    script2 = jupytext.writes(notebook, ext='.py')
+
+    compare(script, script2)
+
+
 def test_round_trip_markdown_cell_with_magic():
     notebook = new_notebook(cells=[new_markdown_cell('IPython has magic commands like\n%quickref')],
                             metadata={'jupytext': {'main_language': 'python'}})