Skip to content

Commit

Permalink
New option --use-source-timestamp (#790)
Browse files Browse the repository at this point in the history
* New option --use-source-timestamp

* Write the output notebook just once
Adjust 'formats', if required, before writing the notebook

* Destination timestamp might be truncated at the micro-second
  • Loading branch information
mwouts authored May 29, 2021
1 parent 1d63611 commit 112ed80
Show file tree
Hide file tree
Showing 3 changed files with 92 additions and 15 deletions.
3 changes: 3 additions & 0 deletions docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ Jupytext ChangeLog
1.11.3 (2021-05-??)
-------------------

**Changed**
- Jupytext CLI has a new option `--use-source-timestamp` that sets the last modification time of the output file equal to that of the source file (this avoids having to change the timestamp of the source file) ([#784](https://github.com/mwouts/jupytext/issues/784))

**Fixed**
- Dependencies of the JupyterLab extension have been upgraded to fix a security vulnerability ([#783](https://github.com/mwouts/jupytext/issues/783))

Expand Down
55 changes: 40 additions & 15 deletions jupytext/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,13 @@ def parse_jupytext_args(args=None):
"See also the --opt and --set-formats options for other ways "
"to operate on the Jupytext metadata.",
)
action.add_argument(
"--use-source-timestamp",
help="Set the modification timestamp of the output file(s) equal"
"to that of the source file, and keep the source file and "
"its timestamp unchanged.",
action="store_true",
)
action.add_argument(
"--warn-only",
"-w",
Expand Down Expand Up @@ -771,7 +778,12 @@ def lazy_write(path, fmt=None, action=None, update_timestamp_only=False):
# Otherwise, we only update the timestamp of the text file to make sure
# they remain more recent than the ipynb file, for compatibility with the
# Jupytext contents manager for Jupyter
if not modified and not path.endswith(".ipynb"):
if args.use_source_timestamp:
log(
f"[jupytext] Setting the timestamp of {shlex.quote(path)} equal to that of {shlex.quote(nb_file)}"
)
os.utime(path, (os.stat(path).st_atime, os.stat(nb_file).st_mtime))
elif not modified and not path.endswith(".ipynb"):
log(f"[jupytext] Updating the timestamp of {shlex.quote(path)}")
os.utime(path, None)

Expand Down Expand Up @@ -808,27 +820,40 @@ def lazy_write(path, fmt=None, action=None, update_timestamp_only=False):
else:
action = ""

lazy_write(nb_dest, fmt=dest_fmt, action=action)
formats = notebook.metadata.get("jupytext", {}).get("formats")
formats = long_form_multiple_formats(formats)
if formats:
try:
base_path_out, _ = find_base_path_and_format(nb_dest, formats)
except InconsistentPath:
# Drop 'formats' if the destination is not part of the paired notebooks
formats = {}
notebook.metadata.get("jupytext", {}).pop("formats")

# c. Synchronize paired notebooks
if args.sync:
write_pair(nb_file, formats, lazy_write)
lazy_write(nb_dest, fmt=dest_fmt, action=action)

elif (
os.path.isfile(nb_file)
and not nb_file.endswith(".ipynb")
and os.path.isfile(nb_dest)
and nb_dest.endswith(".ipynb")
):
formats = notebook.metadata.get("jupytext", {}).get("formats")
if formats is not None and any(
os.path.isfile(alt_path) and os.path.samefile(nb_dest, alt_path)
nb_dest_in_pair = formats and any(
os.path.exists(alt_path) and os.path.samefile(nb_dest, alt_path)
for alt_path, _ in paired_paths(nb_file, fmt, formats)
)

if (
nb_dest_in_pair
and os.path.isfile(nb_file)
and not nb_file.endswith(".ipynb")
and os.path.isfile(nb_dest)
and nb_dest.endswith(".ipynb")
):
# Update the original text file timestamp, as required by our Content Manager
# If the destination is an ipynb file and is in the pair, then we
# update the original text file timestamp, as required by our Content Manager
# Otherwise Jupyter will refuse to open the paired notebook #335
# NB: An alternative is --use-source-timestamp
lazy_write(nb_file, update_timestamp_only=True)

# c. Synchronize paired notebooks
elif args.sync:
write_pair(nb_file, formats, lazy_write)

return untracked_files


Expand Down
49 changes: 49 additions & 0 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -1311,3 +1311,52 @@ def test_jupytext_to_ipynb_does_not_update_timestamp_if_not_paired(

capture = capsys.readouterr()
assert "Updating the timestamp" not in capture.out


@pytest.mark.parametrize("formats", ["ipynb,py", "py:percent", "py", None])
def test_use_source_timestamp(tmpdir, cwd_tmpdir, python_notebook, capsys, formats):
# Write a text notebook
nb = python_notebook
if formats:
nb.metadata["jupytext"] = {"formats": formats}

test_py = tmpdir.join("test.py")
test_ipynb = tmpdir.join("test.ipynb")
write(nb, str(test_py))
src_timestamp = test_py.stat().mtime

# Wait...
time.sleep(0.1)

# py -> ipynb
jupytext(["--to", "ipynb", "test.py", "--use-source-timestamp"])

capture = capsys.readouterr()
assert "Updating the timestamp" not in capture.out

dest_timestamp = test_ipynb.stat().mtime
# on Mac OS the dest_timestamp is truncated at the microsecond (#790)
assert src_timestamp - 1e-6 <= dest_timestamp <= src_timestamp

# Make sure that we can open the file in Jupyter
from jupytext.contentsmanager import TextFileContentsManager

cm = TextFileContentsManager()
cm.outdated_text_notebook_margin = 0.001
cm.root_dir = str(tmpdir)

# No error here
cm.get("test.ipynb")

# But now if we don't use --use-source-timestamp
jupytext(["--to", "ipynb", "test.py"])
os.utime(test_py, (src_timestamp, src_timestamp))

# Then we can't open paired notebooks
if formats == "ipynb,py":
from tornado.web import HTTPError

with pytest.raises(HTTPError, match="seems more recent than test.py"):
cm.get("test.ipynb")
else:
cm.get("test.ipynb")

0 comments on commit 112ed80

Please sign in to comment.