Skip to content

Commit

Permalink
Keeping track of paired notebooks #118
Browse files Browse the repository at this point in the history
This allows to
- keep the original Jupyter behavior: the opened file is the one the user did open
- preserve the timestamp test. When saving, Jupyter will test all the timestamps of all formats of the paired notebook.
  • Loading branch information
mwouts committed Nov 25, 2018
1 parent 47b5494 commit 003154b
Show file tree
Hide file tree
Showing 3 changed files with 69 additions and 7 deletions.
1 change: 1 addition & 0 deletions HISTORY.rst
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ Release History
**Improvements**

- The ``language_info`` section is not part of the default header any more. Language information is now taken from metadata ``kernelspec.language``. (#105).
- When opening a paired notebook, the active file is now the file that was originally opened (#118). When saving a notebook, timestamps of all the alternative representations are tested to ensure that Jupyter's autosave does not override manual modifications.

0.8.5 (2018-11-13)
++++++++++++++++++++++
Expand Down
73 changes: 67 additions & 6 deletions jupytext/contentsmanager.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ class TextFileContentsManager(FileContentsManager, Configurable):
"""

nb_extensions = [ext for ext in NOTEBOOK_EXTENSIONS if ext != '.ipynb']
paired_notebooks = {}

def all_nb_extensions(self):
"""
Expand Down Expand Up @@ -245,6 +246,29 @@ def preferred_format(self, ext, preferred):

return None

def drop_paired_notebook(self, path):
"""Remove the current notebook from the list of paired notebooks"""
if path not in self.paired_notebooks:
return

for alt_path in self.paired_notebooks.pop(path):
if alt_path in self.paired_notebooks:
self.drop_paired_notebook(alt_path)

def update_paired_notebooks(self, path, fmt_group):
"""Update the list of paired notebooks to include/update the current pair"""
if len(fmt_group) <= 1:
self.drop_paired_notebook(path)
return

base_path, fmt, _ = file_fmt_ext(path)
paths = [base_path + fmt for fmt in fmt_group]
for alt_path in paths:
self.drop_paired_notebook(alt_path)

for alt_path in paths:
self.paired_notebooks[alt_path] = paths

def _read_notebook(self, os_path, as_version=4):
"""Read a notebook from an os path."""
_, fmt, ext = file_fmt_ext(os_path)
Expand All @@ -262,6 +286,16 @@ def set_comment_magics_if_none(self, nb):
if self.comment_magics is not None and 'comment_magics' not in nb.metadata.get('jupytext', {}):
nb.metadata.setdefault('jupytext', {})['comment_magics'] = self.comment_magics

def save(self, model, path=''):
"""Save the file model and return the model with no content."""
if model['type'] == 'notebook':
nb = nbformat.from_dict(model['content'])
_, fmt, _ = file_fmt_ext(path)
fmt_group = self.format_group(fmt, nb)
self.update_paired_notebooks(path, fmt_group)

return super(TextFileContentsManager, self).save(model, path)

def _save_notebook(self, os_path, nb):
"""Save a notebook to an os_path."""
self.set_comment_magics_if_none(nb)
Expand All @@ -288,18 +322,31 @@ def get(self, path, content=True, type=None, format=None,

if self.exists(path) and (type == 'notebook' or (type is None and ext in self.all_nb_extensions())):
model = self._notebook_model(path, content=content)
if fmt != ext and content:
model['name'], _ = os.path.splitext(model['name'])
if not content:
if not load_alternative_format:
return model

if not load_alternative_format:
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
for alt_path in self.paired_notebooks.get(path, []):
if alt_path != 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']

return model

fmt_group = self.format_group(fmt, model['content'])
# Otherwise, the modification time of a notebook is the timestamp of the source - see below.

if fmt != ext:
model['name'], _ = os.path.splitext(model['name'])

source_format = fmt
outputs_format = fmt
org_model = model

fmt_group = self.format_group(fmt, model['content'])
self.update_paired_notebooks(path, fmt_group)

# Source format is first non ipynb format found on disk
if fmt.endswith('.ipynb'):
Expand Down Expand Up @@ -383,6 +430,10 @@ def get(self, path, content=True, type=None, format=None,
self.notary.sign(nb)
self.mark_trusted_cells(nb, path)

# 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

return super(TextFileContentsManager, self).get(path, content, type, format)
Expand All @@ -406,8 +457,18 @@ def rename_file(self, old_path, new_path):
old_file, org_fmt, _ = file_fmt_ext(old_path)
new_file, new_fmt, _ = file_fmt_ext(new_path)

alt_paths = self.paired_notebooks.get(old_path, [])
self.drop_paired_notebook(old_path)
self.drop_paired_notebook(new_path)

if org_fmt == new_fmt:
for alt_fmt in self.format_group(org_fmt):
if alt_paths:
fmt_group = [file_fmt_ext(alt_path)[1] for alt_path in alt_paths]
self.update_paired_notebooks(new_path, fmt_group)
else:
fmt_group = self.format_group(org_fmt)

for alt_fmt in fmt_group:
if self.file_exists(old_file + alt_fmt):
super(TextFileContentsManager, self).rename_file(old_file + alt_fmt, new_file + alt_fmt)
else:
Expand Down
2 changes: 1 addition & 1 deletion tests/test_contentsmanager.py
Original file line number Diff line number Diff line change
Expand Up @@ -328,7 +328,7 @@ def test_save_to_light_percent_sphinx_format(nb_file, tmpdir):
# (notebooks not equal as we insert %matplotlib inline in sphinx)

model = cm.get(path=tmp_ipynb)
assert model['name'] == 'notebook.pct'
assert model['name'] == 'notebook.ipynb'
compare_notebooks(nb, model['content'])


Expand Down

0 comments on commit 003154b

Please sign in to comment.