diff --git a/jupytext/__init__.py b/jupytext/__init__.py index fe82e3097..a17bff639 100644 --- a/jupytext/__init__.py +++ b/jupytext/__init__.py @@ -5,6 +5,11 @@ from .reraise import reraise from .version import __version__ +try: + from .contentsmanager import build_jupytext_contents_manager +except ImportError as err: + build_jupytext_contents_manager = reraise(err) + try: from .contentsmanager import TextFileContentsManager except ImportError as err: @@ -13,7 +18,7 @@ def load_jupyter_server_extension(app): # pragma: no cover """Use Jupytext's contents manager""" - if issubclass(app.contents_manager_class, TextFileContentsManager): + if hasattr(app.contents_manager_class, 'default_jupytext_formats'): app.log.info("[Jupytext Server Extension] NotebookApp.contents_manager_class is " "(a subclass of) jupytext.TextFileContentsManager already - OK") return @@ -22,9 +27,9 @@ def load_jupyter_server_extension(app): # pragma: no cover # The contents manager was set at NotebookApp.init_configurables # Let's change the contents manager class - app.log.info('[Jupytext Server Extension] Changing NotebookApp.contents_manager_class ' - 'from {} to jupytext.TextFileContentsManager'.format(app.contents_manager_class.__name__)) - app.contents_manager_class = TextFileContentsManager + app.log.info('[Jupytext Server Extension] Deriving a JupytextContentsManager ' + 'from {}'.format(app.contents_manager_class.__name__)) + app.contents_manager_class = build_jupytext_contents_manager(app.contents_manager_class) try: # And rerun selected init steps from https://github.com/jupyter/notebook/blob/ diff --git a/jupytext/contentsmanager.py b/jupytext/contentsmanager.py index eda851b1f..ab0d4338c 100644 --- a/jupytext/contentsmanager.py +++ b/jupytext/contentsmanager.py @@ -14,12 +14,6 @@ except ImportError: pass -try: - from notebook.services.contents.largefilemanager import LargeFileManager -except ImportError: - # Older versions of notebook do not have the LargeFileManager #217 - from notebook.services.contents.filemanager import FileContentsManager as LargeFileManager - from .jupytext import reads, writes, create_prefix_dir from .combine import combine_inputs_with_outputs from .formats import rearrange_jupytext_metadata, check_file_version @@ -61,394 +55,410 @@ def _reads(text, as_version, **kwargs): return _reads -class TextFileContentsManager(LargeFileManager, Configurable): - """ - A FileContentsManager Class that reads and stores notebooks to classical - Jupyter notebooks (.ipynb), R Markdown notebooks (.Rmd), Julia (.jl), - Python (.py) or R scripts (.R) - """ - - # Dictionary: notebook path => (fmt, formats) where fmt is the current format, and formats the paired formats. - paired_notebooks = dict() - - def all_nb_extensions(self): - """All extensions that should be classified as notebooks""" - return [ext if ext.startswith('.') else '.' + ext for ext in self.notebook_extensions.split(',')] - - default_jupytext_formats = Unicode( - u'', - help='Save notebooks to these file extensions. ' - 'Can be any of ipynb,Rmd,md,jl,py,R,nb.jl,nb.py,nb.R ' - 'comma separated. If you want another format than the ' - 'default one, append the format name to the extension, ' - 'e.g. ipynb,py:percent to save the notebook to ' - 'hydrogen/spyder/vscode compatible scripts', - config=True) - - preferred_jupytext_formats_save = Unicode( - u'', - help='Preferred format when saving notebooks as text, per extension. ' - 'Use "jl:percent,py:percent,R:percent" if you want to save ' - 'Julia, Python and R scripts in the double percent format and ' - 'only write "jupytext_formats": "py" in the notebook metadata.', - config=True) - - preferred_jupytext_formats_read = Unicode( - u'', - help='Preferred format when reading notebooks from text, per ' - 'extension. Use "py:sphinx" if you want to read all python ' - 'scripts as Sphinx gallery scripts.', - config=True) - - default_notebook_metadata_filter = Unicode( - u'', - help="Cell metadata that should be save in the text representations. " - "Examples: 'all', '-all', 'widgets,nteract', 'kernelspec,jupytext-all'", - config=True) - - default_cell_metadata_filter = Unicode( - u'', - help="Notebook metadata that should be saved in the text representations. " - "Examples: 'all', 'hide_input,hide_output'", - config=True) - - comment_magics = Enum( - values=[True, False], - allow_none=True, - help='Should Jupyter magic commands be commented out in the text representation?', - config=True) - - split_at_heading = Bool( - False, - help='Split markdown cells on headings (Markdown and R Markdown formats only)', - config=True) - - sphinx_convert_rst2md = Bool( - False, - help='When opening a Sphinx Gallery script, convert the reStructuredText to markdown', - config=True) - - outdated_text_notebook_margin = Float( - 1.0, - help='Refuse to overwrite inputs of a ipynb notebooks with those of a ' - 'text notebook when the text notebook plus margin is older than ' - 'the ipynb notebook', - config=True) - - default_cell_markers = Unicode( - u'', - help='Start and end cell markers for the light format, comma separated. Use "{{{,}}}" to mark cells' - 'as foldable regions in Vim, and "region,endregion" to mark cells as Vscode/PyCharm regions', - config=True) - - notebook_extensions = Unicode( - u','.join(NOTEBOOK_EXTENSIONS), - help='A comma separated list of notebook extensions', - config=True) - - def drop_paired_notebook(self, path): - """Remove the current notebook from the list of paired notebooks""" - if path not in self.paired_notebooks: - return - - fmt, formats = self.paired_notebooks.pop(path) - prev_paired_paths = paired_paths(path, fmt, formats) - for alt_path, _ in prev_paired_paths: - if alt_path in self.paired_notebooks: +def build_jupytext_contents_manager(BaseContentManager): + """Derives a TextFileContentsManager from the given CM""" + + class JupytextContentsManager(BaseContentManager, Configurable): + """ + A FileContentsManager Class that reads and stores notebooks to classical + Jupyter notebooks (.ipynb), R Markdown notebooks (.Rmd), Julia (.jl), + Python (.py) or R scripts (.R) + """ + + # Dictionary: notebook path => (fmt, formats) where fmt is the current format, and formats the paired formats. + paired_notebooks = dict() + + def all_nb_extensions(self): + """All extensions that should be classified as notebooks""" + return [ext if ext.startswith('.') else '.' + ext for ext in self.notebook_extensions.split(',')] + + default_jupytext_formats = Unicode( + u'', + help='Save notebooks to these file extensions. ' + 'Can be any of ipynb,Rmd,md,jl,py,R,nb.jl,nb.py,nb.R ' + 'comma separated. If you want another format than the ' + 'default one, append the format name to the extension, ' + 'e.g. ipynb,py:percent to save the notebook to ' + 'hydrogen/spyder/vscode compatible scripts', + config=True) + + preferred_jupytext_formats_save = Unicode( + u'', + help='Preferred format when saving notebooks as text, per extension. ' + 'Use "jl:percent,py:percent,R:percent" if you want to save ' + 'Julia, Python and R scripts in the double percent format and ' + 'only write "jupytext_formats": "py" in the notebook metadata.', + config=True) + + preferred_jupytext_formats_read = Unicode( + u'', + help='Preferred format when reading notebooks from text, per ' + 'extension. Use "py:sphinx" if you want to read all python ' + 'scripts as Sphinx gallery scripts.', + config=True) + + default_notebook_metadata_filter = Unicode( + u'', + help="Cell metadata that should be save in the text representations. " + "Examples: 'all', '-all', 'widgets,nteract', 'kernelspec,jupytext-all'", + config=True) + + default_cell_metadata_filter = Unicode( + u'', + help="Notebook metadata that should be saved in the text representations. " + "Examples: 'all', 'hide_input,hide_output'", + config=True) + + comment_magics = Enum( + values=[True, False], + allow_none=True, + help='Should Jupyter magic commands be commented out in the text representation?', + config=True) + + split_at_heading = Bool( + False, + help='Split markdown cells on headings (Markdown and R Markdown formats only)', + config=True) + + sphinx_convert_rst2md = Bool( + False, + help='When opening a Sphinx Gallery script, convert the reStructuredText to markdown', + config=True) + + outdated_text_notebook_margin = Float( + 1.0, + help='Refuse to overwrite inputs of a ipynb notebooks with those of a ' + 'text notebook when the text notebook plus margin is older than ' + 'the ipynb notebook', + config=True) + + default_cell_markers = Unicode( + u'', + help='Start and end cell markers for the light format, comma separated. Use "{{{,}}}" to mark cells' + 'as foldable regions in Vim, and "region,endregion" to mark cells as Vscode/PyCharm regions', + config=True) + + notebook_extensions = Unicode( + u','.join(NOTEBOOK_EXTENSIONS), + help='A comma separated list of notebook extensions', + config=True) + + def drop_paired_notebook(self, path): + """Remove the current notebook from the list of paired notebooks""" + if path not in self.paired_notebooks: + return + + fmt, formats = self.paired_notebooks.pop(path) + prev_paired_paths = paired_paths(path, fmt, formats) + for alt_path, _ in prev_paired_paths: + if alt_path in self.paired_notebooks: + self.drop_paired_notebook(alt_path) + + def update_paired_notebooks(self, path, fmt, formats): + """Update the list of paired notebooks to include/update the current pair""" + if not formats: + self.drop_paired_notebook(path) + return + + new_paired_paths = paired_paths(path, fmt, formats) + for alt_path, _ in new_paired_paths: self.drop_paired_notebook(alt_path) - def update_paired_notebooks(self, path, fmt, formats): - """Update the list of paired notebooks to include/update the current pair""" - if not formats: - self.drop_paired_notebook(path) - return - - new_paired_paths = paired_paths(path, fmt, formats) - for alt_path, _ in new_paired_paths: - self.drop_paired_notebook(alt_path) - - long_formats = long_form_multiple_formats(formats) - if len(long_formats) == 1 and set(long_formats[0]) <= {'extension'}: - return - - short_formats = short_form_multiple_formats(formats) - for alt_path, alt_fmt in new_paired_paths: - self.paired_notebooks[alt_path] = short_form_one_format(alt_fmt), short_formats - - def set_default_format_options(self, format_options, read=False): - """Set default format option""" - if self.default_notebook_metadata_filter: - format_options.setdefault('notebook_metadata_filter', self.default_notebook_metadata_filter) - if self.default_cell_metadata_filter: - format_options.setdefault('cell_metadata_filter', self.default_cell_metadata_filter) - if self.comment_magics is not None: - format_options.setdefault('comment_magics', self.comment_magics) - if self.split_at_heading: - format_options.setdefault('split_at_heading', self.split_at_heading) - if not read and self.default_cell_markers: - format_options.setdefault('cell_markers', self.default_cell_markers) - if read and self.sphinx_convert_rst2md: - format_options.setdefault('rst2md', self.sphinx_convert_rst2md) - - def default_formats(self, path): - """Return the default formats, if they apply to the current path #157""" - formats = long_form_multiple_formats(self.default_jupytext_formats) - for fmt in formats: - try: - base_path(path, fmt) - return self.default_jupytext_formats - except InconsistentPath: - continue - - return None + long_formats = long_form_multiple_formats(formats) + if len(long_formats) == 1 and set(long_formats[0]) <= {'extension'}: + return + + short_formats = short_form_multiple_formats(formats) + for alt_path, alt_fmt in new_paired_paths: + self.paired_notebooks[alt_path] = short_form_one_format(alt_fmt), short_formats + + def set_default_format_options(self, format_options, read=False): + """Set default format option""" + if self.default_notebook_metadata_filter: + format_options.setdefault('notebook_metadata_filter', self.default_notebook_metadata_filter) + if self.default_cell_metadata_filter: + format_options.setdefault('cell_metadata_filter', self.default_cell_metadata_filter) + if self.comment_magics is not None: + format_options.setdefault('comment_magics', self.comment_magics) + if self.split_at_heading: + format_options.setdefault('split_at_heading', self.split_at_heading) + if not read and self.default_cell_markers: + format_options.setdefault('cell_markers', self.default_cell_markers) + if read and self.sphinx_convert_rst2md: + format_options.setdefault('rst2md', self.sphinx_convert_rst2md) + + def default_formats(self, path): + """Return the default formats, if they apply to the current path #157""" + formats = long_form_multiple_formats(self.default_jupytext_formats) + for fmt in formats: + try: + base_path(path, fmt) + return self.default_jupytext_formats + except InconsistentPath: + continue - def create_prefix_dir(self, path, fmt): - """Create the prefix dir, if missing""" - create_prefix_dir(self._get_os_path(path.strip('/')), fmt) + return None - def save(self, model, path=''): - """Save the file model and return the model with no content.""" - if model['type'] != 'notebook': - return super(TextFileContentsManager, self).save(model, path) + def create_prefix_dir(self, path, fmt): + """Create the prefix dir, if missing""" + create_prefix_dir(self._get_os_path(path.strip('/')), fmt) - nbk = model['content'] - try: - metadata = nbk.get('metadata') - rearrange_jupytext_metadata(metadata) - jupytext_metadata = metadata.setdefault('jupytext', {}) - jupytext_formats = jupytext_metadata.get('formats') or self.default_formats(path) + def save(self, model, path=''): + """Save the file model and return the model with no content.""" + if model['type'] != 'notebook': + return super(JupytextContentsManager, self).save(model, path) - if not jupytext_formats: - text_representation = jupytext_metadata.get('text_representation', {}) - ext = os.path.splitext(path)[1] - fmt = {'extension': ext} + nbk = model['content'] + try: + metadata = nbk.get('metadata') + rearrange_jupytext_metadata(metadata) + jupytext_metadata = metadata.setdefault('jupytext', {}) + jupytext_formats = jupytext_metadata.get('formats') or self.default_formats(path) - if ext == text_representation.get('extension') and text_representation.get('format_name'): - fmt['format_name'] = text_representation.get('format_name') + if not jupytext_formats: + text_representation = jupytext_metadata.get('text_representation', {}) + ext = os.path.splitext(path)[1] + fmt = {'extension': ext} - jupytext_formats = [fmt] + if ext == text_representation.get('extension') and text_representation.get('format_name'): + fmt['format_name'] = text_representation.get('format_name') - jupytext_formats = long_form_multiple_formats(jupytext_formats, metadata, - auto_ext_requires_language_info=False) + jupytext_formats = [fmt] - # Set preferred formats if not format name is given yet - jupytext_formats = [preferred_format(f, self.preferred_jupytext_formats_save) for f in jupytext_formats] + jupytext_formats = long_form_multiple_formats(jupytext_formats, metadata, + auto_ext_requires_language_info=False) - base, fmt = find_base_path_and_format(path, jupytext_formats) - self.update_paired_notebooks(path, fmt, jupytext_formats) - self.set_default_format_options(jupytext_metadata) + # Set preferred formats if not format name is given yet + jupytext_formats = [preferred_format(f, self.preferred_jupytext_formats_save) for f in jupytext_formats] - if not jupytext_metadata: - metadata.pop('jupytext') + base, fmt = find_base_path_and_format(path, jupytext_formats) + self.update_paired_notebooks(path, fmt, jupytext_formats) + self.set_default_format_options(jupytext_metadata) - # Save as ipynb first - return_value = None - value = None - for fmt in jupytext_formats[::-1]: - if fmt['extension'] != '.ipynb': - continue + if not jupytext_metadata: + metadata.pop('jupytext') - alt_path = full_path(base, fmt) - self.create_prefix_dir(alt_path, fmt) - self.log.info("Saving %s", os.path.basename(alt_path)) - value = super(TextFileContentsManager, self).save(model, alt_path) - if alt_path == path: - return_value = value - - # And then to the other formats, in reverse order so that - # the first format is the most recent - for fmt in jupytext_formats[::-1]: - if fmt['extension'] == '.ipynb': - continue + # Save as ipynb first + return_value = None + value = None + for fmt in jupytext_formats[::-1]: + if fmt['extension'] != '.ipynb': + continue - alt_path = full_path(base, fmt) - self.create_prefix_dir(alt_path, fmt) - if 'format_name' in fmt and fmt['extension'] not in ['.Rmd', '.md']: - self.log.info("Saving %s in format %s:%s", - os.path.basename(alt_path), fmt['extension'][1:], fmt['format_name']) - else: + alt_path = full_path(base, fmt) + self.create_prefix_dir(alt_path, fmt) self.log.info("Saving %s", os.path.basename(alt_path)) - with mock.patch('nbformat.writes', _jupytext_writes(fmt)): - value = super(TextFileContentsManager, self).save(model, alt_path) + value = super(JupytextContentsManager, self).save(model, alt_path) if alt_path == path: return_value = value - # Update modified timestamp to match that of the pair #207 - return_value['last_modified'] = value['last_modified'] - return return_value - - except Exception as err: - raise HTTPError(400, str(err)) - - def get(self, path, content=True, type=None, format=None, load_alternative_format=True): - """ Takes a path for an entity and returns its model""" - path = path.strip('/') - os_path = self._get_os_path(path) - ext = os.path.splitext(path)[1] - - # Not a notebook? - if (not self.exists(path) or os.path.isdir(os_path) or - (type != 'notebook' if type else ext not in self.all_nb_extensions())): - return super(TextFileContentsManager, self).get(path, content, type, format) - - fmt = preferred_format(ext, self.preferred_jupytext_formats_read) - if ext == '.ipynb': - model = self._notebook_model(path, content=content) - else: - self.set_default_format_options(fmt, read=True) - with mock.patch('nbformat.reads', _jupytext_reads(fmt)): + # And then to the other formats, in reverse order so that + # the first format is the most recent + for fmt in jupytext_formats[::-1]: + if fmt['extension'] == '.ipynb': + continue + + alt_path = full_path(base, fmt) + self.create_prefix_dir(alt_path, fmt) + if 'format_name' in fmt and fmt['extension'] not in ['.Rmd', '.md']: + self.log.info("Saving %s in format %s:%s", + os.path.basename(alt_path), fmt['extension'][1:], fmt['format_name']) + else: + self.log.info("Saving %s", os.path.basename(alt_path)) + with mock.patch('nbformat.writes', _jupytext_writes(fmt)): + value = super(JupytextContentsManager, self).save(model, alt_path) + if alt_path == path: + return_value = value + + # Update modified timestamp to match that of the pair #207 + return_value['last_modified'] = value['last_modified'] + return return_value + + except Exception as err: + raise HTTPError(400, str(err)) + + def get(self, path, content=True, type=None, format=None, load_alternative_format=True): + """ Takes a path for an entity and returns its model""" + path = path.strip('/') + os_path = self._get_os_path(path) + ext = os.path.splitext(path)[1] + + # Not a notebook? + if (not self.exists(path) or os.path.isdir(os_path) or + (type != 'notebook' if type else ext not in self.all_nb_extensions())): + return super(JupytextContentsManager, self).get(path, content, type, format) + + fmt = preferred_format(ext, self.preferred_jupytext_formats_read) + if ext == '.ipynb': model = self._notebook_model(path, content=content) + else: + self.set_default_format_options(fmt, read=True) + with mock.patch('nbformat.reads', _jupytext_reads(fmt)): + model = self._notebook_model(path, content=content) - if not load_alternative_format: - return model - - if not content: - # Modification time of a paired notebook, in this context - Jupyter is checking timestamp - # before saving - is the most recent among all representations #118 - if path not in self.paired_notebooks: + if not load_alternative_format: return model - fmt, formats = self.paired_notebooks.get(path) - for alt_path, _ in paired_paths(path, fmt, formats): - if alt_path != path and self.exists(alt_path): - alt_model = self._notebook_model(alt_path, content=False) - if alt_model['last_modified'] > model['last_modified']: - model['last_modified'] = alt_model['last_modified'] + if not content: + # Modification time of a paired notebook, in this context - Jupyter is checking timestamp + # before saving - is the most recent among all representations #118 + if path not in self.paired_notebooks: + return model - return model + fmt, formats = self.paired_notebooks.get(path) + for alt_path, _ in paired_paths(path, fmt, formats): + if alt_path != path and self.exists(alt_path): + alt_model = self._notebook_model(alt_path, content=False) + if alt_model['last_modified'] > model['last_modified']: + model['last_modified'] = alt_model['last_modified'] - # We will now read a second file if this is a paired notebooks. - nbk = model['content'] - jupytext_formats = nbk.metadata.get('jupytext', {}).get('formats') or self.default_formats(path) - jupytext_formats = long_form_multiple_formats(jupytext_formats, nbk.metadata, - auto_ext_requires_language_info=False) + return model + + # We will now read a second file if this is a paired notebooks. + nbk = model['content'] + jupytext_formats = nbk.metadata.get('jupytext', {}).get('formats') or self.default_formats(path) + jupytext_formats = long_form_multiple_formats(jupytext_formats, nbk.metadata, + auto_ext_requires_language_info=False) + + # Compute paired notebooks from formats + alt_paths = [(path, fmt)] + if jupytext_formats: + try: + _, fmt = find_base_path_and_format(path, jupytext_formats) + alt_paths = paired_paths(path, fmt, jupytext_formats) + self.update_paired_notebooks(path, fmt, jupytext_formats) + except InconsistentPath as err: + self.log.info("Unable to read paired notebook: %s", str(err)) + else: + if path in self.paired_notebooks: + fmt, formats = self.paired_notebooks.get(path) + alt_paths = paired_paths(path, fmt, formats) + + if len(alt_paths) > 1 and ext == '.ipynb': + # Apply default options (like saving and reloading would do) + jupytext_metadata = model['content']['metadata'].get('jupytext', {}) + self.set_default_format_options(jupytext_metadata, read=True) + if jupytext_metadata: + model['content']['metadata']['jupytext'] = jupytext_metadata + + org_model = model + fmt_inputs = fmt + path_inputs = path_outputs = path + model_outputs = None + + # Source format is first non ipynb format found on disk + if path.endswith('.ipynb'): + for alt_path, alt_fmt in alt_paths: + if not alt_path.endswith('.ipynb') and self.exists(alt_path): + self.log.info(u'Reading SOURCE from {}'.format(alt_path)) + path_inputs = alt_path + fmt_inputs = alt_fmt + model_outputs = model + model = self.get(alt_path, content=content, type=type, format=format, + load_alternative_format=False) + break + # Outputs taken from ipynb if in group, if file exists + else: + for alt_path, _ in alt_paths: + if alt_path.endswith('.ipynb') and self.exists(alt_path): + self.log.info(u'Reading OUTPUTS from {}'.format(alt_path)) + path_outputs = alt_path + model_outputs = self.get(alt_path, content=content, type=type, format=format, + load_alternative_format=False) + break - # Compute paired notebooks from formats - alt_paths = [(path, fmt)] - if jupytext_formats: try: - _, fmt = find_base_path_and_format(path, jupytext_formats) - alt_paths = paired_paths(path, fmt, jupytext_formats) - self.update_paired_notebooks(path, fmt, jupytext_formats) - except InconsistentPath as err: - self.log.info("Unable to read paired notebook: %s", str(err)) - else: - if path in self.paired_notebooks: - fmt, formats = self.paired_notebooks.get(path) - alt_paths = paired_paths(path, fmt, formats) - - if len(alt_paths) > 1 and ext == '.ipynb': - # Apply default options (like saving and reloading would do) - jupytext_metadata = model['content']['metadata'].get('jupytext', {}) - self.set_default_format_options(jupytext_metadata, read=True) - if jupytext_metadata: - model['content']['metadata']['jupytext'] = jupytext_metadata - - org_model = model - fmt_inputs = fmt - path_inputs = path_outputs = path - model_outputs = None - - # Source format is first non ipynb format found on disk - if path.endswith('.ipynb'): - for alt_path, alt_fmt in alt_paths: - if not alt_path.endswith('.ipynb') and self.exists(alt_path): - self.log.info(u'Reading SOURCE from {}'.format(alt_path)) - path_inputs = alt_path - fmt_inputs = alt_fmt - model_outputs = model - model = self.get(alt_path, content=content, type=type, format=format, - load_alternative_format=False) - break - # Outputs taken from ipynb if in group, if file exists - else: - for alt_path, _ in alt_paths: - if alt_path.endswith('.ipynb') and self.exists(alt_path): - self.log.info(u'Reading OUTPUTS from {}'.format(alt_path)) - path_outputs = alt_path - model_outputs = self.get(alt_path, content=content, type=type, format=format, - load_alternative_format=False) - break - - try: - check_file_version(model['content'], path_inputs, path_outputs) - except Exception as err: - raise HTTPError(400, str(err)) - - # Before we combine the two files, we make sure we're not overwriting ipynb cells - # with an outdated text file - try: - if model_outputs and model_outputs['last_modified'] > model['last_modified'] + \ - timedelta(seconds=self.outdated_text_notebook_margin): - raise HTTPError( - 400, - '''{out} (last modified {out_last}) - seems more recent than {src} (last modified {src_last}) - Please either: - - open {src} in a text editor, make sure it is up to date, and save it, - - or delete {src} if not up to date, - - or increase check margin by adding, say, - c.ContentsManager.outdated_text_notebook_margin = 5 # in seconds # or float("inf") - to your .jupyter/jupyter_notebook_config.py file - '''.format(src=path_inputs, src_last=model['last_modified'], - out=path_outputs, out_last=model_outputs['last_modified'])) - except OverflowError: - pass - - if model_outputs: - combine_inputs_with_outputs(model['content'], model_outputs['content'], fmt_inputs) - elif not path.endswith('.ipynb'): - set_kernelspec_from_language(model['content']) - - # Trust code cells when they have no output - for cell in model['content'].cells: - if cell.cell_type == 'code' and not cell.outputs and cell.metadata.get('trusted') is False: - cell.metadata['trusted'] = True - - # Path and name of the notebook is the one of the original path - model['path'] = org_model['path'] - model['name'] = org_model['name'] - - return model - - def trust_notebook(self, path): - """Trust the current notebook""" - if path.endswith('.ipynb') or path not in self.paired_notebooks: - super(TextFileContentsManager, self).trust_notebook(path) - return - - fmt, formats = self.paired_notebooks[path] - for alt_path, alt_fmt in paired_paths(path, fmt, formats): - if alt_fmt['extension'] == '.ipynb': - super(TextFileContentsManager, self).trust_notebook(alt_path) - - def rename_file(self, old_path, new_path): - """Rename the current notebook, as well as its alternative representations""" - if old_path not in self.paired_notebooks: + check_file_version(model['content'], path_inputs, path_outputs) + except Exception as err: + raise HTTPError(400, str(err)) + + # Before we combine the two files, we make sure we're not overwriting ipynb cells + # with an outdated text file try: - # we do not know yet if this is a paired notebook (#190) - # -> to get this information we open the notebook - self.get(old_path, content=True) - except Exception: + if model_outputs and model_outputs['last_modified'] > model['last_modified'] + \ + timedelta(seconds=self.outdated_text_notebook_margin): + raise HTTPError( + 400, + '''{out} (last modified {out_last}) + seems more recent than {src} (last modified {src_last}) + Please either: + - open {src} in a text editor, make sure it is up to date, and save it, + - or delete {src} if not up to date, + - or increase check margin by adding, say, + c.ContentsManager.outdated_text_notebook_margin = 5 # in seconds # or float("inf") + to your .jupyter/jupyter_notebook_config.py file + '''.format(src=path_inputs, src_last=model['last_modified'], + out=path_outputs, out_last=model_outputs['last_modified'])) + except OverflowError: pass - if old_path not in self.paired_notebooks: - super(TextFileContentsManager, self).rename_file(old_path, new_path) - return + if model_outputs: + combine_inputs_with_outputs(model['content'], model_outputs['content'], fmt_inputs) + elif not path.endswith('.ipynb'): + set_kernelspec_from_language(model['content']) + + # Trust code cells when they have no output + for cell in model['content'].cells: + if cell.cell_type == 'code' and not cell.outputs and cell.metadata.get('trusted') is False: + cell.metadata['trusted'] = True - fmt, formats = self.paired_notebooks.get(old_path) - old_alt_paths = paired_paths(old_path, fmt, formats) + # Path and name of the notebook is the one of the original path + model['path'] = org_model['path'] + model['name'] = org_model['name'] - # Is the new file name consistent with suffix? - try: - new_base = base_path(new_path, fmt) - except Exception as err: - raise HTTPError(400, str(err)) + return model + + def trust_notebook(self, path): + """Trust the current notebook""" + if path.endswith('.ipynb') or path not in self.paired_notebooks: + super(JupytextContentsManager, self).trust_notebook(path) + return + + fmt, formats = self.paired_notebooks[path] + for alt_path, alt_fmt in paired_paths(path, fmt, formats): + if alt_fmt['extension'] == '.ipynb': + super(JupytextContentsManager, self).trust_notebook(alt_path) + + def rename_file(self, old_path, new_path): + """Rename the current notebook, as well as its alternative representations""" + if old_path not in self.paired_notebooks: + try: + # we do not know yet if this is a paired notebook (#190) + # -> to get this information we open the notebook + self.get(old_path, content=True) + except Exception: + pass + + if old_path not in self.paired_notebooks: + super(JupytextContentsManager, self).rename_file(old_path, new_path) + return + + fmt, formats = self.paired_notebooks.get(old_path) + old_alt_paths = paired_paths(old_path, fmt, formats) + + # Is the new file name consistent with suffix? + try: + new_base = base_path(new_path, fmt) + except Exception as err: + raise HTTPError(400, str(err)) - for old_alt_path, alt_fmt in old_alt_paths: - new_alt_path = full_path(new_base, alt_fmt) - if self.exists(old_alt_path): - super(TextFileContentsManager, self).rename_file(old_alt_path, new_alt_path) + for old_alt_path, alt_fmt in old_alt_paths: + new_alt_path = full_path(new_base, alt_fmt) + if self.exists(old_alt_path): + super(JupytextContentsManager, self).rename_file(old_alt_path, new_alt_path) + + self.drop_paired_notebook(old_path) + self.update_paired_notebooks(new_path, fmt, formats) + + return JupytextContentsManager + + +try: + from notebook.services.contents.largefilemanager import LargeFileManager + + TextFileContentsManager = build_jupytext_contents_manager(LargeFileManager) +except ImportError: + # Older versions of notebook do not have the LargeFileManager #217 + from notebook.services.contents.filemanager import FileContentsManager - self.drop_paired_notebook(old_path) - self.update_paired_notebooks(new_path, fmt, formats) + TextFileContentsManager = build_jupytext_contents_manager(LargeFileManager)