diff --git a/README.md b/README.md index 932cc01b1..003ce7fa0 100644 --- a/README.md +++ b/README.md @@ -66,7 +66,10 @@ inputs. ## Can I save my Jupyter notebook as both R markdown and ipynb? Yes. That's useful if you want to preserve the outputs locally, or if you want -to share the `.ipynb` version. We offer both per-notebook, and global configuration. +to share the `.ipynb` version. By default, the opened notebook in jupyter, plus +its `.ipynb` version, are updated when a notebook is saved. + +If you prefer a different setting, we offer both per-notebook, and global configuration. ### Per-notebook configuration @@ -93,12 +96,10 @@ Accepted formats are: `.ipynb`, `.Rmd` and `.md`. If you want every notebook to be saved as both `.Rmd` and `.ipynb` files, then change your jupyter config to ```python c.NotebookApp.contents_manager_class = 'nbrmd.RmdFileContentsManager' -c.ContentsManager.pre_save_hook = 'nbrmd.update_rmd_and_ipynb' +c.ContentsManager.default_nbrmd_formats = ['.ipynb', '.Rmd'] ``` -If you prefer to update just one of `.Rmd` or `.ipynb` files, then change the above to -`nbrmd.update_rmd` or `nbrmd.update_ipynb` as the `pre_save_hook` (and yes, you're free to use the `pre_save_hook` -with the default `ContentsManager`). +If you prefer to update just `.Rmd`, change the above accordingly. :warning: Be careful not to open twice a notebook with two distinct extensions! You should _shutdown_ the notebooks with the extension you are not currently editing (list your open notebooks with the _running_ tab in Jupyter). diff --git a/nbrmd/__init__.py b/nbrmd/__init__.py index bfcd19718..781f5e940 100644 --- a/nbrmd/__init__.py +++ b/nbrmd/__init__.py @@ -12,7 +12,6 @@ """ from .nbrmd import readf, writef, writes, reads, notebook_extensions, readme -from .hooks import * try: from .rmarkdownexporter import RMarkdownExporter diff --git a/nbrmd/cm.py b/nbrmd/cm.py index 77075f781..66e85651e 100644 --- a/nbrmd/cm.py +++ b/nbrmd/cm.py @@ -1,8 +1,8 @@ import notebook.transutils from notebook.services.contents.filemanager import FileContentsManager from tornado.web import HTTPError -from .combine import combine_inputs_with_outputs -from .hooks import update_selected_formats +import hooks +import combine import os import nbrmd @@ -30,14 +30,17 @@ class RmdFileContentsManager(FileContentsManager): Jupyter notebooks (.ipynb), or in R Markdown (.Rmd), plain markdown (.md), R scripts (.R) or python scripts (.py) """ + nb_extensions = [ext for ext in nbrmd.notebook_extensions if ext != '.ipynb'] def all_nb_extensions(self): return ['.ipynb'] + self.nb_extensions + default_nbrmd_formats = ['.ipynb'] + def __init__(self, **kwargs): - self.pre_save_hook = update_selected_formats + self.pre_save_hook = hooks.update_alternative_formats super(RmdFileContentsManager, self).__init__(**kwargs) def _read_notebook(self, os_path, as_version=4): @@ -81,8 +84,9 @@ def get(self, path, content=True, type=None, format=None): try: nb_outputs = self._notebook_model( path_ipynb, content=content) - combine_inputs_with_outputs(nb['content'], - nb_outputs['content']) + combine.combine_inputs_with_outputs( + nb['content'], + nb_outputs['content']) except HTTPError: pass diff --git a/nbrmd/hooks.py b/nbrmd/hooks.py index cb87e2741..3ba5139af 100644 --- a/nbrmd/hooks.py +++ b/nbrmd/hooks.py @@ -1,43 +1,18 @@ import os import nbrmd import nbformat +import cm -def check_extensions(extensions): - if extensions is None: - extensions = [] - if isinstance(extensions, str): - extensions = [extensions] - if not isinstance(extensions, list) or not set(extensions).issubset( - nbrmd.notebook_extensions): - raise TypeError('Notebook extensions ' - 'should be a subset of {},' - 'but are {}'.format(str(nbrmd.notebook_extensions), - str(extensions))) - return extensions - - -def update_formats(extensions=None): - """A function that generates a pre_save_hook for the desired extensions""" - extensions = check_extensions(extensions) - - def pre_save_hook(model, path, contents_manager=None, **kwargs): - return update_selected_formats(model, path, - contents_manager, - extensions=extensions, **kwargs) - - return pre_save_hook - - -def update_selected_formats(model, path, contents_manager=None, - extensions=None, **kwargs): +def update_alternative_formats(model, path, contents_manager=None, **kwargs): """ - A pre-save hook for jupyter that saves notebooks to multiple files - with the desired extensions. + A pre-save hook for jupyter that saves the notebooks + under the alternative form. Target extensions are taken from + notebook metadata 'nbrmd_formats', or when not available, + from contents_manager.default_nbrmd_formats :param model: data model, that may contain the notebook :param path: full name for ipython notebook :param contents_manager: ContentsManager instance - :param extensions: list of alternative formats :param kwargs: not used :return: """ @@ -51,28 +26,22 @@ def update_selected_formats(model, path, contents_manager=None, if nb['nbformat'] != 4: return - extensions = check_extensions(extensions) - extensions = (nb.get('metadata', {} - ).get('nbrmd_formats', extensions) - or extensions) + formats = contents_manager.default_nbrmd_formats \ + if isinstance(contents_manager, cm.RmdFileContentsManager) else ['.ipynb'] + formats = nb.get('metadata', {}).get('nbrmd_formats', formats) + if not isinstance(formats, list) or not set(formats).issubset( + ['.Rmd', '.md', '.ipynb']): + raise TypeError(u"Notebook metadata 'nbrmd_formats' " + u"should be subset of ['.Rmd', '.md', '.ipynb']") os_path = contents_manager._get_os_path(path) if contents_manager else path file, ext = os.path.splitext(path) os_file, ext = os.path.splitext(os_path) - for alt_ext in extensions: + for alt_ext in formats: if ext != alt_ext: if contents_manager: contents_manager.log.info( u"Saving file at /%s", file + alt_ext) nbrmd.writef(nbformat.notebooknode.from_dict(nb), os_file + alt_ext) - - -update_rmd_and_ipynb = update_formats(['.ipynb', '.Rmd']) -update_ipynb = update_formats('.ipynb') -update_rmd = update_formats('.Rmd') -update_md = update_formats('.md') -update_py = update_formats('.py') -update_py_and_ipynb = update_formats(['.ipynb', '.py']) -update_R = update_formats('.R') diff --git a/tests/test_jupyter_hook.py b/tests/test_jupyter_hook.py index 69e146344..b2d54bb72 100644 --- a/tests/test_jupyter_hook.py +++ b/tests/test_jupyter_hook.py @@ -11,7 +11,10 @@ def test_rmd_is_ok(nb_file, tmpdir): tmp_ipynb = str(tmpdir.join('notebook.ipynb')) tmp_rmd = str(tmpdir.join('notebook.Rmd')) - nbrmd.update_rmd(model=dict(type='notebook', content=nb), path=tmp_ipynb) + nb.metadata['nbrmd_formats'] = ['.Rmd'] + nbrmd.update_alternative_formats( + model=dict(type='notebook', content=nb), + path=tmp_ipynb) nb2 = nbrmd.readf(tmp_rmd) @@ -24,7 +27,9 @@ def test_ipynb_is_ok(nb_file, tmpdir): tmp_ipynb = str(tmpdir.join('notebook.ipynb')) tmp_rmd = str(tmpdir.join('notebook.Rmd')) - nbrmd.update_ipynb(model=dict(type='notebook', content=nb), path=tmp_rmd) + nbrmd.update_alternative_formats( + model=dict(type='notebook', content=nb), + path=tmp_rmd) nb2 = nbrmd.readf(tmp_ipynb) @@ -39,8 +44,9 @@ def test_all_files_created(nb_file, tmpdir): tmp_rmd = str(tmpdir.join('notebook.Rmd')) nb.metadata['nbrmd_formats'] = ['.Rmd', '.ipynb', '.md'] - nbrmd.update_selected_formats( - model=dict(type='notebook', content=nb), path=tmp_ipynb) + nbrmd.update_alternative_formats( + model=dict(type='notebook', content=nb), + path=tmp_ipynb) nb2 = nbrmd.readf(tmp_md) assert remove_outputs_and_header(nb) == remove_outputs_and_header(nb2) @@ -54,7 +60,7 @@ def test_no_files_created_on_no_format(tmpdir): tmp_md = str(tmpdir.join('notebook.md')) tmp_rmd = str(tmpdir.join('notebook.Rmd')) - nbrmd.update_selected_formats( + nbrmd.update_alternative_formats( model=dict(type='notebook', content=dict(nbformat=4, metadata=dict())), path=tmp_ipynb) @@ -67,7 +73,7 @@ def test_raise_on_wrong_format(tmpdir): tmp_ipynb = str(tmpdir.join('notebook.ipynb')) with pytest.raises(TypeError): - nbrmd.update_selected_formats( + nbrmd.update_alternative_formats( model=dict(type='notebook', content=dict(nbformat=4, metadata=dict(nbrmd_formats=['.doc']))), @@ -78,7 +84,7 @@ def test_no_rmd_on_not_notebook(tmpdir): tmp_ipynb = str(tmpdir.join('notebook.ipynb')) tmp_rmd = str(tmpdir.join('notebook.Rmd')) - nbrmd.update_rmd(model=dict(type='not notebook'), path=tmp_ipynb) + nbrmd.update_alternative_formats(model=dict(type='not notebook'), path=tmp_ipynb) assert not os.path.isfile(tmp_rmd) @@ -86,7 +92,8 @@ def test_no_rmd_on_not_v4(tmpdir): tmp_ipynb = str(tmpdir.join('notebook.ipynb')) tmp_rmd = str(tmpdir.join('notebook.Rmd')) - nbrmd.update_rmd( - model=dict(type='notebook', content=dict(nbformat=3)), path=tmp_ipynb) + nbrmd.update_alternative_formats( + model=dict(type='notebook', content=dict(nbformat=3)), + path=tmp_rmd) - assert not os.path.isfile(tmp_rmd) + assert not os.path.isfile(tmp_ipynb)