From ebf50b6b52bce59c2026b0c7be71275fa9f265b6 Mon Sep 17 00:00:00 2001 From: Marc Wouts Date: Tue, 29 Oct 2019 23:31:02 +0100 Subject: [PATCH] R Markdown visibility options are mapped to Jupyter Book ones By default. And a `use_runtools` option is provided to map them to the runtools options instead. Closes #337 --- jupytext/cell_metadata.py | 70 ++++++++++--------- jupytext/cell_reader.py | 5 +- jupytext/cell_to_text.py | 8 +-- jupytext/formats.py | 2 +- jupytext/jupytext.py | 5 ++ tests/test_active_cells.py | 8 +-- tests/test_cell_metadata.py | 8 +-- ...est_hide_remove_input_outputs_rmarkdown.py | 31 +++++++- 8 files changed, 88 insertions(+), 49 deletions(-) diff --git a/jupytext/cell_metadata.py b/jupytext/cell_metadata.py index b80e41588..c2463858b 100644 --- a/jupytext/cell_metadata.py +++ b/jupytext/cell_metadata.py @@ -30,6 +30,13 @@ (('results', "'hide'"), [('hide_output', True)]), (('results', '"hide"'), [('hide_output', True)]) ] +# Alternatively, Jupytext can also map the Jupyter Book options to R Markdown +_RMARKDOWN_TO_JUPYTER_BOOK_MAP = [ + (('include', 'FALSE'), 'remove_cell'), + (('echo', 'FALSE'), 'remove_input'), + (('results', "'hide'"), 'remove_output'), + (('results', '"hide"'), 'remove_output') +] _JUPYTEXT_CELL_METADATA = [ # Pre-jupytext metadata @@ -65,22 +72,25 @@ def _py_logical_values(rbool): raise RLogicalValueError -def metadata_to_rmd_options(language, metadata): - """ - Convert language and metadata information to their rmd representation - :param language: - :param metadata: - :return: - """ +def metadata_to_rmd_options(language, metadata, use_runtools=False): + """Convert language and metadata information to their rmd representation""" options = (language or 'R').lower() if 'name' in metadata: options += ' ' + metadata['name'] + ',' del metadata['name'] - for rmd_option, jupyter_options in _RMARKDOWN_TO_RUNTOOLS_OPTION_MAP: - if all([metadata.get(opt_name) == opt_value for opt_name, opt_value in jupyter_options]): - options += ' {}={},'.format(rmd_option[0], 'FALSE' if rmd_option[1] is False else rmd_option[1]) - for opt_name, _ in jupyter_options: - metadata.pop(opt_name) + if use_runtools: + for rmd_option, jupyter_options in _RMARKDOWN_TO_RUNTOOLS_OPTION_MAP: + if all([metadata.get(opt_name) == opt_value for opt_name, opt_value in jupyter_options]): + options += ' {}={},'.format(rmd_option[0], 'FALSE' if rmd_option[1] is False else rmd_option[1]) + for opt_name, _ in jupyter_options: + metadata.pop(opt_name) + else: + for rmd_option, tag in _RMARKDOWN_TO_JUPYTER_BOOK_MAP: + if tag in metadata.get('tags', []): + options += ' {}={},'.format(rmd_option[0], 'FALSE' if rmd_option[1] is False else rmd_option[1]) + metadata['tags'] = [i for i in metadata['tags'] if i != tag] + if not metadata['tags']: + metadata.pop('tags') for opt_name in metadata: opt_value = metadata[opt_name] opt_name = opt_name.strip() @@ -100,19 +110,19 @@ def metadata_to_rmd_options(language, metadata): return options.strip(',').strip() -def update_metadata_from_rmd_options(name, value, metadata): - """ - Update metadata using the _BOOLEAN_OPTIONS_DICTIONARY mapping - :param name: option name - :param value: option value - :param metadata: - :return: - """ - for rmd_option, jupyter_options in _RMARKDOWN_TO_RUNTOOLS_OPTION_MAP: - if name == rmd_option[0] and value == rmd_option[1]: - for opt_name, opt_value in jupyter_options: - metadata[opt_name] = opt_value - return True +def update_metadata_from_rmd_options(name, value, metadata, use_runtools=False): + """Map the R Markdown cell visibility options to the Jupyter ones""" + if use_runtools: + for rmd_option, jupyter_options in _RMARKDOWN_TO_RUNTOOLS_OPTION_MAP: + if name == rmd_option[0] and value == rmd_option[1]: + for opt_name, opt_value in jupyter_options: + metadata[opt_name] = opt_value + return True + else: + for rmd_option, tag in _RMARKDOWN_TO_JUPYTER_BOOK_MAP: + if name == rmd_option[0] and value == rmd_option[1]: + metadata.setdefault('tags', []).append(tag) + return True return False @@ -213,12 +223,8 @@ def parse_rmd_options(line): return result -def rmd_options_to_metadata(options): - """ - Parse rmd options and return a metadata dictionary - :param options: - :return: - """ +def rmd_options_to_metadata(options, use_runtools=False): + """Parse rmd options and return a metadata dictionary""" options = re.split(r'\s|,', options, 1) if len(options) == 1: language = options[0] @@ -236,7 +242,7 @@ def rmd_options_to_metadata(options): if i == 0 and name == '': metadata['name'] = value continue - if update_metadata_from_rmd_options(name, value, metadata): + if update_metadata_from_rmd_options(name, value, metadata, use_runtools=use_runtools): continue try: metadata[name] = _py_logical_values(value) diff --git a/jupytext/cell_reader.py b/jupytext/cell_reader.py index e5ff6038a..307a96676 100644 --- a/jupytext/cell_reader.py +++ b/jupytext/cell_reader.py @@ -98,6 +98,7 @@ def __init__(self, fmt=None, default_language=None): self.ext = fmt.get('extension') self.default_language = default_language or _SCRIPT_EXTENSIONS.get(self.ext, {}).get('language', 'python') self.comment_magics = fmt.get('comment_magics', self.default_comment_magics) + self.use_runtools = fmt.get('use_runtools', False) self.format_version = fmt.get('format_version') self.metadata = None self.org_content = [] @@ -398,7 +399,7 @@ class RMarkdownCellReader(MarkdownCellReader): default_comment_magics = True def options_to_metadata(self, options): - return rmd_options_to_metadata(options) + return rmd_options_to_metadata(options, self.use_runtools) def uncomment_code_and_magics(self, lines): if self.cell_type == 'code' and self.comment_magics and is_active(self.ext, self.metadata): @@ -436,7 +437,7 @@ class RScriptCellReader(ScriptCellReader): default_comment_magics = True def options_to_metadata(self, options): - return rmd_options_to_metadata('r ' + options) + return rmd_options_to_metadata('r ' + options, self.use_runtools) def find_cell_end(self, lines): """Return position of end of cell marker, and position diff --git a/jupytext/cell_to_text.py b/jupytext/cell_to_text.py index 2d02d167d..7474b823f 100644 --- a/jupytext/cell_to_text.py +++ b/jupytext/cell_to_text.py @@ -52,9 +52,9 @@ def __init__(self, cell, default_language, fmt=None): self.language = self.language or default_language self.default_language = default_language self.comment = _SCRIPT_EXTENSIONS.get(self.ext, {}).get('comment', '#') - self.comment_magics = self.fmt['comment_magics'] if 'comment_magics' in self.fmt \ - else self.default_comment_magics + self.comment_magics = self.fmt.get('comment_magics', self.default_comment_magics) self.cell_metadata_json = self.fmt.get('cell_metadata_json', False) + self.use_runtools = self.fmt.get('use_runtools', False) # how many blank lines before next cell self.lines_to_next_cell = cell.metadata.get('lines_to_next_cell') @@ -206,7 +206,7 @@ def code_to_text(self): lines = [] if not is_active(self.ext, self.metadata): self.metadata['eval'] = False - options = metadata_to_rmd_options(self.language, self.metadata) + options = metadata_to_rmd_options(self.language, self.metadata, self.use_runtools) lines.append('```{{{}}}'.format(options)) lines.extend(source) lines.append('```') @@ -373,7 +373,7 @@ def code_to_text(self): lines = [] if not is_active(self.ext, self.metadata): self.metadata['eval'] = False - options = metadata_to_rmd_options(None, self.metadata) + options = metadata_to_rmd_options(None, self.metadata, self.use_runtools) if options: lines.append('#+ {}'.format(options)) lines.extend(source) diff --git a/jupytext/formats.py b/jupytext/formats.py index 515e19154..f28494ce4 100644 --- a/jupytext/formats.py +++ b/jupytext/formats.py @@ -534,7 +534,7 @@ def short_form_multiple_formats(jupytext_formats): _VALID_FORMAT_INFO = ['extension', 'format_name', 'suffix', 'prefix'] -_BINARY_FORMAT_OPTIONS = ['comment_magics', 'split_at_heading', 'rst2md', 'cell_metadata_json'] +_BINARY_FORMAT_OPTIONS = ['comment_magics', 'split_at_heading', 'rst2md', 'cell_metadata_json', 'use_runtools'] _VALID_FORMAT_OPTIONS = _BINARY_FORMAT_OPTIONS + ['notebook_metadata_filter', 'cell_metadata_filter', 'cell_markers'] diff --git a/jupytext/jupytext.py b/jupytext/jupytext.py index 15dbf0d03..5379a91f4 100644 --- a/jupytext/jupytext.py +++ b/jupytext/jupytext.py @@ -132,6 +132,11 @@ def writes(self, nb, metadata=None, **kwargs): metadata = nb.metadata default_language = default_language_from_metadata_and_ext(metadata, self.implementation.extension) or 'python' self.update_fmt_with_notebook_options(nb.metadata) + if 'use_runtools' not in self.fmt: + for cell in nb.cells: + if cell.metadata.get('hide_input', False) or cell.metadata.get('hide_output', False): + self.fmt['use_runtools'] = True + break if 'main_language' in metadata.get('jupytext', {}): del metadata['jupytext']['main_language'] diff --git a/tests/test_active_cells.py b/tests/test_active_cells.py index f4ec96206..a63ce7d88 100644 --- a/tests/test_active_cells.py +++ b/tests/test_active_cells.py @@ -233,22 +233,20 @@ def test_active_rmd(ext, no_jupytext_version_number): compare(nb.cells[0], ACTIVE_RMD['.ipynb']) -ACTIVE_NOT_INCLUDE_RMD = {'.py': """# + hide_input=true hide_output=true active="Rmd" +ACTIVE_NOT_INCLUDE_RMD = {'.py': """# + tags=["remove_cell"] active="Rmd" # # This cell is active in Rmd only """, '.Rmd': """```{python include=FALSE, active="Rmd"} # This cell is active in Rmd only ``` """, - '.R': """# + hide_input=true hide_output=true active="Rmd" + '.R': """# + tags=["remove_cell"] active="Rmd" # # This cell is active in Rmd only """, '.ipynb': {'cell_type': 'raw', 'source': '# This cell is active in Rmd only', - 'metadata': {'active': 'Rmd', - 'hide_input': True, - 'hide_output': True}}} + 'metadata': {'active': 'Rmd', 'tags': ['remove_cell']}}} @skip_if_dict_is_not_ordered diff --git a/tests/test_cell_metadata.py b/tests/test_cell_metadata.py index c2ffe5eb1..0245eff5a 100644 --- a/tests/test_cell_metadata.py +++ b/tests/test_cell_metadata.py @@ -13,7 +13,7 @@ ('R', {'name': 'plot_1', 'bool': True, 'fig.path': "'fig_path/'"})), ('r echo=FALSE', - ('R', {'hide_input': True})), + ('R', {'tags': ['remove_input']})), ('r plot_1, echo=TRUE', ('R', {'name': 'plot_1', 'echo': True})), ('python echo=if a==5 then TRUE else FALSE', @@ -24,10 +24,10 @@ ('python active="ipynb,py"', ('python', {'active': 'ipynb,py'})), ('python include=FALSE, active="Rmd"', - ('python', {'active': 'Rmd', 'hide_output': True, 'hide_input': True})), + ('python', {'active': 'Rmd', 'tags': ['remove_cell']})), ('r chunk_name, include=FALSE, active="Rmd"', ('R', - {'name': 'chunk_name', 'active': 'Rmd', 'hide_output': True, 'hide_input': True})), + {'name': 'chunk_name', 'active': 'Rmd', 'tags': ['remove_cell']})), ('python tags=c("parameters")', ('python', {'tags': ['parameters']}))] @@ -63,7 +63,7 @@ def test_parsing_error(options): def test_ignore_metadata(): - metadata = {'trusted': True, 'hide_input': True} + metadata = {'trusted': True, 'tags': ['remove_input']} metadata = filter_metadata(metadata, None, _IGNORE_CELL_METADATA) assert metadata_to_rmd_options('R', metadata) == 'r echo=FALSE' diff --git a/tests/test_hide_remove_input_outputs_rmarkdown.py b/tests/test_hide_remove_input_outputs_rmarkdown.py index 954b37034..93a33e80b 100644 --- a/tests/test_hide_remove_input_outputs_rmarkdown.py +++ b/tests/test_hide_remove_input_outputs_rmarkdown.py @@ -4,6 +4,35 @@ from .utils import skip_if_dict_is_not_ordered +@skip_if_dict_is_not_ordered +@pytest.mark.parametrize('md,rmd', [('tags=["remove_cell"]', 'include=FALSE'), + ('tags=["remove_output"]', "results='hide'"), + ('tags=["remove_output"]', 'results="hide"'), + ('tags=["remove_input"]', 'echo=FALSE')]) +def test_jupyter_book_options_to_rmarkdown(md, rmd): + """By default, Jupyter Book tags are mapped to R Markdown options, and vice versa #337""" + md = '```python ' + md + """ +1 + 1 +``` +""" + + rmd = '```{python ' + rmd + """} +1 + 1 +``` +""" + + nb_md = jupytext.reads(md, 'md') + nb_rmd = jupytext.reads(rmd, 'Rmd') + compare_notebooks(nb_rmd, nb_md) + + md2 = jupytext.writes(nb_rmd, 'md') + compare(md2, md) + + rmd = rmd.replace('"hide"', "'hide'") + rmd2 = jupytext.writes(nb_md, 'Rmd') + compare(rmd2, rmd) + + @skip_if_dict_is_not_ordered @pytest.mark.parametrize('md,rmd', [('hide_input=true hide_output=true', 'include=FALSE'), ('hide_output=true', "results='hide'"), @@ -23,7 +52,7 @@ def test_runtools_options_to_rmarkdown(md, rmd): """ nb_md = jupytext.reads(md, 'md') - nb_rmd = jupytext.reads(rmd, 'Rmd') + nb_rmd = jupytext.reads(rmd, fmt={'extension': '.Rmd', 'use_runtools': True}) compare_notebooks(nb_rmd, nb_md) md2 = jupytext.writes(nb_rmd, 'md')