Skip to content

Commit

Permalink
New option --use-source-timestamp
Browse files Browse the repository at this point in the history
  • Loading branch information
mwouts committed May 20, 2021
1 parent 697bfb1 commit b9cdd8c
Show file tree
Hide file tree
Showing 3 changed files with 87 additions and 14 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)

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

Expand Down
50 changes: 36 additions & 14 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 @@ -810,25 +822,35 @@ def lazy_write(path, fmt=None, action=None, update_timestamp_only=False):

lazy_write(nb_dest, fmt=dest_fmt, action=action)

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

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 is not None 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 formats is not None and not nb_dest_in_pair:
# We remove the formats if the destination is not in the pair (and rewrite,
# this is not great, but samefile above requires that the file exists)
notebook.metadata.get("jupytext", {}).pop("formats")
lazy_write(nb_dest, fmt=dest_fmt, action=action)

if (
os.path.isfile(nb_file)
and not nb_file.endswith(".ipynb")
and os.path.isfile(nb_dest)
and nb_dest.endswith(".ipynb")
and nb_dest_in_pair
):
# 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
48 changes: 48 additions & 0 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -1311,3 +1311,51 @@ 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
assert src_timestamp == dest_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 run into trouble
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 b9cdd8c

Please sign in to comment.