Skip to content

Commit

Permalink
Command line tool revisited #44 #46
Browse files Browse the repository at this point in the history
  • Loading branch information
mwouts committed Aug 31, 2018
1 parent 172d8ff commit 90a6eb4
Show file tree
Hide file tree
Showing 6 changed files with 96 additions and 210 deletions.
1 change: 1 addition & 0 deletions HISTORY.rst
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ easier to read. Pylint score at 9.9 !
one blank space. Two blank spaces are allowed as well. Now you can reformat
code in Python IDE without breaking notebook cells (#38).
- Added support for plain markdown files (#40, #44).
- Command line tool simpler to use (#46).
- Start code patterns present in Jupyter cells are escaped.
- Default `nbrmd_format` is empty (mwouts/nbsrc/#5): no Jupyter notebook
is created on disk when the user opens a Python or R file and saves it from
Expand Down
32 changes: 17 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,34 +90,36 @@ configuration, and instead edit the notebook metadata as follows:
}
```

Accepted formats should have these extensions: `ipynb`, `md`, Rmd`, `py` and `R`.
Accepted formats should have these extensions: `ipynb`, `md`, `Rmd`, `py` and `R`.

In case you want both `py` and `Rmd`, please note that the
order matters: the first non-`ipynb` extension
is the one used as the reference source for notebook inputs when you open the `ipynb` file.

## Command line conversion

The package provides two `nbrmd` and `nbsrc` scripts that convert Jupyter notebooks to R markdown notebooks and scripts, and vice-versa.
The package provides a `nbrmd` script for command line conversion between the various notebook extensions.

Use them as:
```bash
nbrmd jupyter.ipynb # this prints the Rmarkdown alternative
nbrmd jupyter.ipynb -i # this creates a jupyter.Rmd file
nbrmd jupyter.Rmd -i # and this, a jupyter.ipynb file
nbrmd jupyter.Rmd -i -p # update the jupyter.ipynb file and preserve outputs that correspond to unchanged inputs

nbsrc jupyter.ipynb # this prints the `.py` or `.R` alternative
nbsrc jupyter.ipynb -i # this creates a jupyter.py or jupyter.R file
nbsrc jupyter.py -i # and this, a jupyter.ipynb file
nbsrc jupyter.py -i -p # update the jupyter.ipynb file and preserve outputs that correspond to unchanged inputs
nbrmd notebook.ipynb md --test # Test round trip conversion
nbrmd notebook.ipynb md # display the markdown version on screen

nbrmd notebook.ipynb .md # create a notebook.md file
nbrmd notebook.ipynb .py # create a notebook.py file
nbrmd notebook.ipynb notebook.py # create a notebook.py file

nbrmd notebook.md .ipynb # overwrite notebook.ipynb (remove outputs)
nbrmd notebook.md .ipynb --update # update notebook.ipynb (preserve outputs)

nbrmd notebook1.md notebook2.py .ipynb # overwrite notebook1.ipynb notebook2.ipynb
```

Alternatively, the `nbrmd` package provides a few `nbconvert` exporters:
```bash
nbconvert jupyter.ipynb --to rmarkdown
nbconvert jupyter.ipynb --to pynotebook
nbconvert jupyter.ipynb --to rnotebook
nbconvert notebook.ipynb --to rmarkdown
nbconvert notebook.ipynb --to pynotebook
nbconvert notebook.ipynb --to rnotebook
```

## Usefull cell metadata
Expand All @@ -128,4 +130,4 @@ nbconvert jupyter.ipynb --to rnotebook

## Jupyter magics

Jupyter magics are escaped in the script and R markdown representations so that scripts can actually be executed. Comment a magic with `#noescape` on the same line to avoid escaping. User defined magics can be escaped with `#escape`.
Jupyter magics are escaped in the script and R markdown representations so that scripts can actually be executed. Comment a magic with `#noescape` on the same line to avoid escaping. User defined magics can be escaped with `#escape`. Magics are not escaped in the plain markdown representation.
142 changes: 55 additions & 87 deletions nbrmd/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,73 +3,62 @@

import os
import argparse
from nbformat import writes as ipynb_writes
from nbrmd import readf, writef
from nbrmd import writes
from .languages import default_language_from_metadata_and_ext
from nbrmd import readf, writef, writes
from nbrmd import NOTEBOOK_EXTENSIONS
from .combine import combine_inputs_with_outputs
from .compare import test_round_trip_conversion
from .file_format_version import check_file_version


def convert(nb_files, markdown, in_place=True,
test_round_trip=False, combine=True):
def convert_notebook_files(nb_files, nb_dest,
test_round_trip=False, preserve_outputs=True):
"""
Export R markdown notebooks, python or R scripts, or Jupyter notebooks,
to the opposite format
:param nb_files: one or more notebooks
:param markdown: R markdown to Jupyter, or scripts to Jupyter?
:param in_place: should result of conversion be stored in file
with opposite extension?
:param nb_files: one or more notebooks files
:param nb_dest: destination file, extension ('.py') or format ('py')
:param test_round_trip: should round trip conversion be tested?
:param combine: should the current outputs of .ipynb file be preserved,
when a cell with corresponding input is found in .Rmd/.py or .R file?
:param preserve_outputs: preserve the current outputs of .ipynb file
when possible
:return:
"""

if len(nb_files) > 1 and nb_dest not in NOTEBOOK_EXTENSIONS:
raise ValueError(
"Converting multiple files requires "
"that destination be one of '{}'".format(
"', '".join(NOTEBOOK_EXTENSIONS)))

for nb_file in nb_files:
file, ext = os.path.splitext(nb_file)
if markdown:
fmt = 'R Markdown'
if ext not in ['.ipynb', '.Rmd']:
raise TypeError(
'File {} is neither a Jupyter (.ipynb) nor a '
'R Markdown (.Rmd) notebook'.format(nb_file))
else:
fmt = 'source'
if ext not in ['.ipynb', '.py', '.R']:
raise TypeError(
'File {} is neither a Jupyter (.ipynb) nor a '
'python script (.py), nor a R script (.R)'.format(nb_file))
file, current_ext = os.path.splitext(nb_file)
if current_ext not in NOTEBOOK_EXTENSIONS:
raise TypeError('File {} is not a notebook'.format(nb_file))

notebook = readf(nb_file)
main_language = default_language_from_metadata_and_ext(notebook, ext)
ext_dest = '.Rmd' if markdown else '.R' \
if main_language == 'R' else '.py'
dest, dest_ext = os.path.splitext(nb_dest)
if not dest_ext:
dest = file
if nb_dest in NOTEBOOK_EXTENSIONS:
dest_ext = nb_dest
else:
dest_ext = '.' + nb_dest

if test_round_trip:
test_round_trip_conversion(notebook, ext_dest, combine)
if dest_ext not in NOTEBOOK_EXTENSIONS:
raise TypeError('Destination extension {} is not a notebook'
.format(dest_ext))

if in_place:
if ext == '.ipynb':
nb_dest = file + ext_dest
print('Jupyter notebook "{}" being converted to '
'{} "{}"'.format(nb_file, fmt, nb_dest))
else:
nb_dest = file + '.ipynb'
print('{} "{}" being converted to '
'Jupyter notebook "{}"'
.format(fmt.title(), nb_file, nb_dest))
if test_round_trip:
test_round_trip_conversion(notebook, dest_ext, preserve_outputs)

convert_notebook_in_place(notebook, nb_file, nb_dest, combine)
if '.' in nb_dest:
save_notebook_as(notebook, nb_file, dest + dest_ext,
preserve_outputs)
elif not test_round_trip:
if ext == '.ipynb':
print(writes(notebook, ext=ext_dest))
else:
print(ipynb_writes(notebook))
print(writes(notebook, ext=dest_ext))


def convert_notebook_in_place(notebook, nb_file, nb_dest, combine):
"""File to file notebook conversion"""
def save_notebook_as(notebook, nb_file, nb_dest, combine):
"""Save notebook to file, in desired format"""
if combine and os.path.isfile(nb_dest) and \
os.path.splitext(nb_dest)[1] == '.ipynb':
check_file_version(notebook, nb_file, nb_dest)
Expand All @@ -79,53 +68,32 @@ def convert_notebook_in_place(notebook, nb_file, nb_dest, combine):
writef(notebook, nb_dest)


def add_common_arguments(parser):
"""Add the common nbrmd and nbsrc arguments"""
parser.add_argument('-p', '--preserve_outputs', action='store_true',
help='Preserve outputs of .ipynb '
'notebook when file exists and inputs match')
group = parser.add_mutually_exclusive_group()
parser.add_argument('-i', '--in-place', action='store_true',
help='Store the result of conversion '
'to file with opposite extension')
group.add_argument('-t', '--test', action='store_true',
help='Test whether the notebook or script is '
'preserved in a round trip conversion')


def cli_nbrmd(args=None):
"""Command line parser for nbrmd"""
parser = argparse.ArgumentParser(description='Jupyter notebook '
'from/to R Markdown')
parser = argparse.ArgumentParser(
description='Jupyter notebooks as markdown documents, '
'Python or R scripts')
parser.add_argument('notebooks',
help='One or more .ipynb or .Rmd notebook(s) '
'to be converted to the alternate form',
help='One or more notebook(s) '
'to be converted, with extension among'
"'{}'".format("', '".join(NOTEBOOK_EXTENSIONS)),
nargs='+')
add_common_arguments(parser)
return parser.parse_args(args)


def cli_nbsrc(args=None):
"""Command line parser for nbsrc"""
parser = argparse.ArgumentParser(description='Jupyter notebook '
'from/to R or Python script')
parser.add_argument('notebooks',
help='One or more .ipynb or .R or .py script(s) '
'to be converted to the alternate form',
nargs='+')
add_common_arguments(parser)
parser.add_argument('to',
help="Destination notebook 'notebook.md', "
"extension '.md', "
"or format 'md' (for on screen display)")
parser.add_argument('-p', '--preserve_outputs', action='store_true',
help='Preserve outputs of .ipynb destination '
'(when file exists and inputs match)')
parser.add_argument('--test', dest='test', action='store_true',
help='Test that notebook is stable under '
'round trip conversion')
return parser.parse_args(args)


def nbsrc(args=None):
"""Entry point for the nbsrc script"""
args = cli_nbsrc(args)
convert(args.notebooks, False, args.in_place,
args.test, args.preserve_outputs)


def nbrmd(args=None):
"""Entry point for the nbrmd script"""
args = cli_nbrmd(args)
convert(args.notebooks, True, args.in_place, args.test,
args.preserve_outputs)
convert_notebook_files(nb_files=args.notebooks, nb_dest=args.to,
test_round_trip=args.test,
preserve_outputs=args.preserve_outputs)
5 changes: 2 additions & 3 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,12 @@
version='0.6.0',
author='Marc Wouts',
author_email='[email protected]',
description='Jupyter notebooks as R markdown, Python or R scripts',
description='Jupyter notebooks as markdown documents, Python or R scripts',
long_description=long_description,
long_description_content_type='text/markdown',
url='https://github.com/mwouts/nbrmd',
packages=find_packages(),
entry_points={'console_scripts': ['nbrmd = nbrmd.cli:nbrmd',
'nbsrc = nbrmd.cli:nbsrc'],
entry_points={'console_scripts': ['nbrmd = nbrmd.cli:nbrmd'],
'nbconvert.exporters':
['rmarkdown = nbrmd:RMarkdownExporter',
'pynotebook = nbrmd:PyNotebookExporter',
Expand Down
44 changes: 21 additions & 23 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
@@ -1,43 +1,39 @@
import os
from shutil import copyfile
import pytest
from testfixtures import compare
import mock
from nbformat.v4.nbbase import new_notebook
from nbrmd import readf, writef, file_format_version
from nbrmd.cli import convert as convert_, cli_nbrmd as cli, nbsrc, nbrmd
from nbrmd import readf, writef, writes, file_format_version
from nbrmd.cli import convert_notebook_files, cli_nbrmd, nbrmd
from nbrmd.compare import compare_notebooks
from .utils import list_all_notebooks

file_format_version.FILE_FORMAT_VERSION = {}


def convert(*args, **kwargs):
return convert_(*args, markdown=True, **kwargs)


@pytest.mark.parametrize('nb_file',
list_all_notebooks('.ipynb') +
list_all_notebooks('.Rmd'))
def test_cli_single_file(nb_file):
assert cli([nb_file]).notebooks == [nb_file]
assert cli_nbrmd([nb_file] + ['py']).notebooks == [nb_file]


@pytest.mark.parametrize('nb_files', [list_all_notebooks('.ipynb') +
list_all_notebooks('.Rmd')])
def test_cli_multiple_files(nb_files):
assert cli(nb_files).notebooks == nb_files
assert cli_nbrmd(nb_files + ['py']).notebooks == nb_files


@pytest.mark.parametrize('nb_file',
list_all_notebooks('.ipynb') +
list_all_notebooks('.Rmd'))
list_all_notebooks('.ipynb'))
def test_convert_single_file_in_place(nb_file, tmpdir):
nb_org = str(tmpdir.join(os.path.basename(nb_file)))
base, ext = os.path.splitext(nb_org)
nb_other = base + '.ipynb' if ext == '.Rmd' else base + '.Rmd'
nb_other = base + '.py'

copyfile(nb_file, nb_org)
convert([nb_org])
convert_notebook_files([nb_org], nb_dest='.py')

nb1 = readf(nb_org)
nb2 = readf(nb_other)
Expand All @@ -49,29 +45,30 @@ def test_convert_single_file_in_place(nb_file, tmpdir):
list_all_notebooks('.ipynb') +
list_all_notebooks('.Rmd'))
def test_convert_single_file(nb_file, capsys):
convert([nb_file], in_place=False)
nb1 = readf(nb_file)
pynb = writes(nb1, ext='.py')
convert_notebook_files([nb_file], nb_dest='py')

out, err = capsys.readouterr()
assert out != ''
assert err == ''
compare(out[:-1], pynb)


@pytest.mark.parametrize('nb_files',
[list_all_notebooks('.ipynb') +
list_all_notebooks('.Rmd')])
[list_all_notebooks('.ipynb')])
def test_convert_multiple_file(nb_files, tmpdir):
nb_orgs = []
nb_others = []

for nb_file in nb_files:
nb_org = str(tmpdir.join(os.path.basename(nb_file)))
base, ext = os.path.splitext(nb_org)
nb_other = base + '.ipynb' if ext == '.Rmd' else base + '.Rmd'
nb_other = base + '.py'
copyfile(nb_file, nb_org)
nb_orgs.append(nb_org)
nb_others.append(nb_other)

convert(nb_orgs)
convert_notebook_files(nb_orgs, '.py')

for nb_org, nb_other in zip(nb_orgs, nb_others):
nb1 = readf(nb_org)
Expand All @@ -81,7 +78,7 @@ def test_convert_multiple_file(nb_files, tmpdir):

def test_error_not_notebook(nb_file='notebook.ext'):
with pytest.raises(TypeError):
convert([nb_file])
convert_notebook_files([nb_file], '.py')


def test_combine_same_version_ok(tmpdir):
Expand All @@ -104,11 +101,12 @@ def test_combine_same_version_ok(tmpdir):

with mock.patch('nbrmd.file_format_version.FILE_FORMAT_VERSION',
{'.py': '1.0'}):
nbsrc(args=[tmp_nbpy, '-i', '-p'])
# to jupyter notebook
nbrmd(args=[tmp_nbpy, '.ipynb', '-p'])
# test round trip
nbsrc(args=[tmp_nbpy, '-t'])
nbrmd(args=[tmp_nbpy, '.ipynb', '--test'])
# test ipynb to rmd
nbrmd(args=[tmp_ipynb, '-i'])
nbrmd(args=[tmp_ipynb, '.Rmd'])

nb = readf(tmp_ipynb)
cells = nb['cells']
Expand Down Expand Up @@ -143,4 +141,4 @@ def test_combine_lower_version_raises(tmpdir):
with pytest.raises(ValueError):
with mock.patch('nbrmd.file_format_version.FILE_FORMAT_VERSION',
{'.py': '1.0'}):
nbsrc(args=[tmp_nbpy, '-i', '-p'])
nbrmd(args=[tmp_nbpy, '.ipynb', '-p'])
Loading

0 comments on commit 90a6eb4

Please sign in to comment.