Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New ignore argument & config option #988

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions docs/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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)
-------------------

Expand Down
50 changes: 46 additions & 4 deletions jupytext/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import glob
import json
import os
import pathlib
import re
import shlex
import subprocess
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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


Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand Down
18 changes: 12 additions & 6 deletions jupytext/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
help="The formats to which notebooks should be saved - a coma separated list."
help="The formats to which notebooks should be saved - a comma 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
)
Expand Down
2 changes: 1 addition & 1 deletion jupytext/version.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
"""Jupytext's version number"""

__version__ = "1.14.1"
__version__ = "1.14.2-dev"
9 changes: 7 additions & 2 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import os
import unittest.mock as mock
from pathlib import Path

Expand Down Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Expand Down
59 changes: 59 additions & 0 deletions tests/test_ignore.py
Original file line number Diff line number Diff line change
@@ -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"