diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index ac70a8fd2..d78c38943 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -1,6 +1,16 @@ Jupytext ChangeLog ================== +1.14.2 (2022-07-??) +------------------- + +**Added** +- New `ignore` option for both the command line version of `jupytext` and the configuration files. The value of `ignore` can be a glob or a list of glob expressions ([#986](https://github.com/mwouts/jupytext/issues/986)) + +**Changed** +- `jupytext --sync` won't issue a warning when an unpaired file is passed as an argument (except if no paired file is found at all) ([#986](https://github.com/mwouts/jupytext/issues/986)) + + 1.14.1 (2022-07-29) ------------------- diff --git a/jupytext/cli.py b/jupytext/cli.py index dcb1b9630..522562242 100644 --- a/jupytext/cli.py +++ b/jupytext/cli.py @@ -4,6 +4,7 @@ import glob import json import os +import pathlib import re import shlex import subprocess @@ -40,6 +41,10 @@ from .version import __version__ +class IgnoredFile(ValueError): + pass + + def system(*args, **kwargs): """Execute the given bash command""" kwargs.setdefault("stdout", subprocess.PIPE) @@ -77,6 +82,12 @@ def parse_jupytext_args(args=None): "Notebook is read from stdin when this argument is empty.", nargs="*", ) + parser.add_argument( + "--ignore", + "-i", + help="One or more glob that should be ignored", + nargs="*", + ) parser.add_argument( "--from", dest="input_format", @@ -483,15 +494,35 @@ def single_line(msg, *args, **kwargs): # Count how many file have round-trip issues when testing exit_code = 0 + unpaired_files = [] + ignored_files = [] for nb_file in notebooks: if not args.warn_only: - exit_code += jupytext_single_file(nb_file, args, log) + try: + exit_code += jupytext_single_file(nb_file, args, log) + except IgnoredFile: + ignored_files.append(nb_file) + except NotAPairedNotebook: + log(f"[jupytext] {nb_file} is not a paired notebook") + unpaired_files.append(nb_file) else: try: exit_code += jupytext_single_file(nb_file, args, log) + except IgnoredFile: + ignored_files.append(nb_file) + except NotAPairedNotebook: + log(f"[jupytext] {nb_file} is not a paired notebook") + unpaired_files.append(nb_file) except Exception as err: sys.stderr.write(f"[jupytext] Error: {str(err)}\n") + if len(ignored_files) == len(notebooks): + sys.stderr.write("Warning: all input files were ignored\n") + elif len(unpaired_files) + len(ignored_files) == len(notebooks): + sys.stderr.write( + f"[jupytext] Warning: no paired file was found among {unpaired_files}\n" + ) + return exit_code @@ -526,6 +557,20 @@ def jupytext_single_file(nb_file, args, log): config = load_jupytext_config(os.path.abspath(nb_file)) + for pattern in args.ignore or config.ignore: + if "*" in pattern or "?" in pattern: + ignored_files = glob.glob(pattern, recursive=True) + else: + ignored_files = pattern + + nb_file_path = pathlib.Path(nb_file) + for ignored in ignored_files: + if ( + nb_file_path.exists() and nb_file_path.samefile(ignored) + ) or nb_file_path == ignored: + log(f"[jupytext] Ignoring {nb_file}") + raise IgnoredFile() + # Just acting on metadata / pipe => save in place save_in_place = not nb_dest and not args.sync if save_in_place: @@ -641,9 +686,6 @@ def jupytext_single_file(nb_file, args, log): notebook, fmt, config, formats, nb_file, log, args.pre_commit_mode ) nb_files = [inputs_nb_file, outputs_nb_file] - except NotAPairedNotebook as err: - sys.stderr.write("[jupytext] Warning: " + str(err) + "\n") - return 0 except InconsistentVersions as err: sys.stderr.write("[jupytext] Error: " + str(err) + "\n") return 1 diff --git a/jupytext/config.py b/jupytext/config.py index dd489900e..836ee1580 100644 --- a/jupytext/config.py +++ b/jupytext/config.py @@ -47,14 +47,20 @@ class JupytextConfiguration(Configurable): formats = Union( [Unicode(), List(Unicode()), Dict(Unicode())], - 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", + help="The formats to which notebooks should be saved - a coma separated list." + "Use ipynb,py:percent to pair ipynb notebooks to py files in the percent format," + "ipynb,md,auto:percent to pair ipynb notebooks to both md files and scripts in " + "the percent format. The option also accept file prefix and suffix, see the full" + "documentation at https://jupytext.readthedocs.io/en/latest/config.html", config=True, ) + + ignore = List( + Unicode(), + help="A list of glob patterns. Any file among these patterns will be ignored.", + config=True, + ) + default_jupytext_formats = Unicode( help="Deprecated. Use 'formats' instead", config=True ) diff --git a/jupytext/version.py b/jupytext/version.py index af9e52db4..ae77350b8 100644 --- a/jupytext/version.py +++ b/jupytext/version.py @@ -1,3 +1,3 @@ """Jupytext's version number""" -__version__ = "1.14.1" +__version__ = "1.14.2-dev" diff --git a/tests/conftest.py b/tests/conftest.py index 5c0cc79e9..8f4870f28 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,3 +1,4 @@ +import os import unittest.mock as mock from pathlib import Path @@ -41,8 +42,12 @@ def cwd_tmpdir(tmpdir): @pytest.fixture() def cwd_tmp_path(tmp_path): # Run the whole test from inside tmp_path - with tmp_path.cwd(): - yield tmp_path + prev_cwd = Path.cwd() + os.chdir(tmp_path) + try: + yield + finally: + os.chdir(prev_cwd) @pytest.fixture diff --git a/tests/test_cli.py b/tests/test_cli.py index 1f09e1e73..d0b82ccd7 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -517,7 +517,7 @@ def test_sync(nb_file, tmpdir, cwd_tmpdir, capsys): # Test that sync issues a warning when the notebook is not paired jupytext(["--sync", tmp_ipynb]) _, err = capsys.readouterr() - assert "is not a paired notebook" in err + assert "no paired file was found" in err # Now with a pairing information nb.metadata.setdefault("jupytext", {})["formats"] = "py,Rmd,ipynb" @@ -570,7 +570,7 @@ def test_sync_pandoc(nb_file, tmpdir, cwd_tmpdir, capsys): # Test that sync issues a warning when the notebook is not paired jupytext(["--sync", tmp_ipynb]) _, err = capsys.readouterr() - assert "is not a paired notebook" in err + assert "no paired file was found" in err # Now with a pairing information nb.metadata.setdefault("jupytext", {})["formats"] = "ipynb,md:pandoc" diff --git a/tests/test_ignore.py b/tests/test_ignore.py new file mode 100644 index 000000000..1f51c95ee --- /dev/null +++ b/tests/test_ignore.py @@ -0,0 +1,59 @@ +import pytest + +from jupytext import TextFileContentsManager +from jupytext.cli import jupytext + + +def test_ignore_works_on_a_non_existing_file(tmp_path, cwd_tmp_path, capsys): + jupytext(["test.py", "--to", "ipynb", "--ignore", "test*.py"]) + + +def test_warning_when_all_files_ignored(tmp_path, cwd_tmp_path, capsys): + (tmp_path / "test.py").write_text("# to be ignored\n") + jupytext(["test.py", "--to", "ipynb", "--ignore", "test*.py"]) + _, err = capsys.readouterr() + assert "Warning: all input files were ignored" in err + + jupytext(["test.py", "--set-formats", "ipynb,py:percent", "--ignore", "test*.py"]) + _, err = capsys.readouterr() + assert "Warning: all input files were ignored" in err + + +@pytest.mark.parametrize( + "command", [["--to", "ipynb"], ["--set-formats", "ipynb,py:percent"]] +) +def test_ignored_files_are_ignored_at_the_cli(tmp_path, cwd_tmp_path, command): + (tmp_path / "nb.py").write_text("# %%\n1 + 1\n") + (tmp_path / "test.py").write_text("# to be ignored\n") + + jupytext(["nb.py", "test.py", "--ignore", "test*.py"] + command) + assert (tmp_path / "nb.ipynb").exists() + assert not (tmp_path / "test.ipynb").exists() + + +@pytest.mark.parametrize( + "command", [["--to", "ipynb"], ["--set-formats", "ipynb,py:percent"]] +) +def test_ignore_files_through_config(tmp_path, cwd_tmp_path, command): + (tmp_path / "jupytext.toml").write_text('ignore = ["test*.py"]') + (tmp_path / "nb.py").write_text("# %%\n1 + 1\n") + (tmp_path / "test.py").write_text("# to be ignored\n") + + jupytext(["nb.py", "test.py"] + command) + assert (tmp_path / "nb.ipynb").exists() + assert not (tmp_path / "test.ipynb").exists() + + +def test_ignored_files_are_not_notebooks(tmp_path, cwd_tmp_path): + (tmp_path / "jupytext.toml").write_text('ignore = ["test*.py"]') + (tmp_path / "nb.py").write_text("# %%\n1 + 1\n") + (tmp_path / "test.py").write_text("# to be ignored\n") + + cm = TextFileContentsManager() + cm.root_dir = str(tmp_path) + + model = cm.get("nb.py") + assert model["type"] == "notebook" + + model = cm.get("test.py") + assert model["type"] == "text"