diff --git a/HISTORY.rst b/HISTORY.rst index 82e5066a5..93f3eb95f 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -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 diff --git a/README.md b/README.md index bd8681645..b6b84625e 100644 --- a/README.md +++ b/README.md @@ -90,7 +90,7 @@ 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 @@ -98,26 +98,28 @@ is the one used as the reference source for notebook inputs when you open the `i ## 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 @@ -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. diff --git a/nbrmd/cli.py b/nbrmd/cli.py index 839aef839..c51f0bbfa 100644 --- a/nbrmd/cli.py +++ b/nbrmd/cli.py @@ -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) @@ -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) diff --git a/setup.py b/setup.py index ad792772e..3d66b2b1d 100644 --- a/setup.py +++ b/setup.py @@ -10,13 +10,12 @@ version='0.6.0', author='Marc Wouts', author_email='marc.wouts@gmail.com', - 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', diff --git a/tests/test_cli.py b/tests/test_cli.py index d6690b381..c15d87bbe 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -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) @@ -49,16 +45,17 @@ 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 = [] @@ -66,12 +63,12 @@ def test_convert_multiple_file(nb_files, tmpdir): 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) @@ -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): @@ -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'] @@ -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']) diff --git a/tests/test_cli_nbsrc.py b/tests/test_cli_nbsrc.py deleted file mode 100644 index 6ec240750..000000000 --- a/tests/test_cli_nbsrc.py +++ /dev/null @@ -1,82 +0,0 @@ -import os -from shutil import copyfile -import pytest -import nbrmd -from nbrmd.cli import convert as convert_, cli_nbsrc as cli -from nbrmd.compare import compare_notebooks -from .utils import list_all_notebooks - -nbrmd.file_format_version.FILE_FORMAT_VERSION = {} - - -def convert(*args, **kwargs): - return convert_(*args, markdown=False, **kwargs) - - -@pytest.mark.parametrize('nb_file', - list_all_notebooks('.ipynb') + - list_all_notebooks('.py')) -def test_cli_single_file(nb_file): - assert cli([nb_file]).notebooks == [nb_file] - - -@pytest.mark.parametrize('nb_files', [list_all_notebooks('.ipynb') + - list_all_notebooks('.py')]) -def test_cli_multiple_files(nb_files): - assert cli(nb_files).notebooks == nb_files - - -@pytest.mark.parametrize('nb_file', - list_all_notebooks('.ipynb') + - list_all_notebooks('.py')) -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 == '.py' else base + '.py' - - copyfile(nb_file, nb_org) - convert([nb_org]) - - nb1 = nbrmd.readf(nb_org) - nb2 = nbrmd.readf(nb_other) - - compare_notebooks(nb1, nb2) - - -@pytest.mark.parametrize('nb_file', - list_all_notebooks('.ipynb') + - list_all_notebooks('.py')) -def test_convert_single_file(nb_file, capsys): - convert([nb_file], in_place=False) - - out, err = capsys.readouterr() - assert out != '' - assert err == '' - - -@pytest.mark.parametrize('nb_files', - [list_all_notebooks('.ipynb') + - list_all_notebooks('.py')]) -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 == '.py' else base + '.py' - copyfile(nb_file, nb_org) - nb_orgs.append(nb_org) - nb_others.append(nb_other) - - convert(nb_orgs) - - for nb_org, nb_other in zip(nb_orgs, nb_others): - nb1 = nbrmd.readf(nb_org) - nb2 = nbrmd.readf(nb_other) - compare_notebooks(nb1, nb2) - - -def test_error_not_notebook(nb_file='notebook.ext'): - with pytest.raises(TypeError): - convert([nb_file])