Skip to content

Commit

Permalink
pre-commit hook #121
Browse files Browse the repository at this point in the history
  • Loading branch information
mwouts committed Nov 30, 2018
1 parent d9f0250 commit 7fb93bb
Show file tree
Hide file tree
Showing 3 changed files with 131 additions and 4 deletions.
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,13 @@ jupytext --to md --output - notebook.ipynb # display the markdown version o
jupytext --from ipynb --to py:percent # read ipynb from stdin and write double percent script on stdout
```

Jupytext is also available as a pre-commit hook. Use this if you want Jupytext to create and update the `.py` representation of your `.ipynb` notebooks on every commit. All you need is to create a `.git/hooks/pre-commit` file with the following content:
```bash
#!/bin/sh
jupytext --to py:light --pre-commit
```
Jupytext does not offer a merge driver. If a conflict occurs, solve it on the text representation and then update or recreate the `.ipynb` notebook. Or give a try to nbdime and its [merge driver](https://nbdime.readthedocs.io/en/stable/vcs.html#merge-driver).

## Reading notebooks in Python

Manipulate notebooks in a Python shell or script using `jupytext`'s main functions:
Expand Down
53 changes: 50 additions & 3 deletions jupytext/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@
"""

import os
import re
import sys
import subprocess
import argparse
from .jupytext import readf, reads, writef, writes
from .formats import NOTEBOOK_EXTENSIONS, JUPYTEXT_FORMATS, check_file_version, one_format_as_string, parse_one_format
Expand All @@ -12,7 +14,7 @@
from .version import __version__


def convert_notebook_files(nb_files, fmt, input_format=None, output=None,
def convert_notebook_files(nb_files, fmt, input_format=None, output=None, pre_commit=False,
test_round_trip=False, test_round_trip_strict=False, stop_on_first_error=True,
update=True, freeze_metadata=False, comment_magics=None):
"""
Expand All @@ -22,6 +24,7 @@ def convert_notebook_files(nb_files, fmt, input_format=None, output=None,
:param input_format: input format, e.g. "py:percent"
:param fmt: destination format, e.g. "py:percent"
:param output: None, destination file, or '-' for stdout
:param pre_commit: convert notebooks in the git index?
:param test_round_trip: should round trip conversion be tested?
:param test_round_trip_strict: should round trip conversion be tested, with strict notebook comparison?
:param stop_on_first_error: when testing, should we stop on first error, or compare the full notebook?
Expand All @@ -36,6 +39,23 @@ def convert_notebook_files(nb_files, fmt, input_format=None, output=None,
if ext not in NOTEBOOK_EXTENSIONS:
raise TypeError('Destination extension {} is not a notebook'.format(ext))

if pre_commit:
input_format = input_format or 'ipynb'
input_ext, _ = parse_one_format(input_format)
modified, deleted = modified_and_deleted_files(input_ext)

for file in modified:
dest_file = file[:-len(input_ext)] + ext
nb = readf(file)
writef(nb, dest_file, format_name=format_name)
system('git', 'add', dest_file)

for file in deleted:
dest_file = file[:-len(input_ext)] + ext
system('git', 'rm', dest_file)

return

if not nb_files:
if not input_format:
raise ValueError('Reading notebook from the standard input requires the --from field.')
Expand Down Expand Up @@ -116,6 +136,22 @@ def convert_notebook_files(nb_files, fmt, input_format=None, output=None,
exit(notebooks_in_error)


def system(*args, **kwargs):
"""Execute the given bash command"""
kwargs.setdefault('stdout', subprocess.PIPE)
proc = subprocess.Popen(args, **kwargs)
out, err = proc.communicate()
return out


def modified_and_deleted_files(ext):
"""Return the list of modified and deleted ipynb files in the git index"""
re_modified = re.compile(r'^[AM]+\s+(?P<name>.*{})'.format(ext.replace('.', r'\.')), re.MULTILINE)
re_deleted = re.compile(r'^[D]+\s+(?P<name>.*{})'.format(ext.replace('.', r'\.')), re.MULTILINE)
files = system('git', 'status', '--porcelain').decode('utf-8')
return re_modified.findall(files), re_deleted.findall(files)


def save_notebook_as(notebook, nb_file, nb_dest, format_name, combine):
"""Save notebook to file, in desired format"""
if combine and os.path.isfile(nb_dest) and os.path.splitext(nb_dest)[1] == '.ipynb':
Expand Down Expand Up @@ -187,6 +223,10 @@ def cli_jupytext(args=None):
'provided , but then the --from field is '
'mandatory',
nargs='*')
parser.add_argument('--pre-commit', action='store_true',
help="""Run Jupytext on the ipynb files in the git index. Use Jupytext
as a pre-commit hook with: echo '#!/bin/sh
jupytext --to py:light --pre-commit' > .git/hooks/pre-commit""")
parser.add_argument('-o', '--output',
help='Destination file. Defaults to original file, '
'with extension changed to destination format. '
Expand Down Expand Up @@ -227,12 +267,18 @@ def cli_jupytext(args=None):
args.output = '-'

if not args.input_format:
if not args.notebooks:
raise ValueError('Please specificy either --from or notebooks')
if not args.notebooks and not args.pre_commit:
raise ValueError('Please specificy either --from, --pre-commit or notebooks')

if args.update and not (args.test or args.test_strict) and args.to != 'ipynb':
raise ValueError('--update works exclusively with --to notebook ')

if args.pre_commit:
if args.notebooks:
raise ValueError('--pre-commit takes notebooks from the git index. Do not pass any notebook here.')
if args.test or args.test_strict:
raise ValueError('--pre-commit cannot be used with --test or --test-strict')

return args


Expand All @@ -249,6 +295,7 @@ def jupytext(args=None):
fmt=args.to,
input_format=args.input_format,
output=args.output,
pre_commit=args.pre_commit,
test_round_trip=args.test,
test_round_trip_strict=args.test_strict,
stop_on_first_error=args.stop_on_first_error,
Expand Down
75 changes: 74 additions & 1 deletion tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from nbformat.v4.nbbase import new_notebook
from jupytext import header, __version__
from jupytext import readf, writef, writes
from jupytext.cli import convert_notebook_files, cli_jupytext, jupytext
from jupytext.cli import convert_notebook_files, cli_jupytext, jupytext, system
from jupytext.compare import compare_notebooks
from .utils import list_notebooks

Expand Down Expand Up @@ -244,3 +244,76 @@ def test_convert_to_percent_format_and_keep_magics(nb_file, tmpdir):
nb2 = readf(tmp_nbpy)

compare_notebooks(nb1, nb2)


def test_pre_commit_hook(tmpdir):
tmp_ipynb = str(tmpdir.join('notebook.ipynb'))
tmp_py = str(tmpdir.join('notebook.py'))
nb = new_notebook(cells=[])

def git(*args):
print(system('git', *args, cwd=str(tmpdir)))

git('init')
git('status')
with open(str(tmpdir.join('.git/hooks/pre-commit')), 'w') as fp:
fp.write('#!/bin/sh\n'
'jupytext --to py:light --pre-commit\n')

writef(nb, tmp_ipynb)
assert os.path.isfile(tmp_ipynb)
assert not os.path.isfile(tmp_py)

git('add', 'notebook.ipynb')
git('status')
git('commit', '-m', 'created')
git('status')

assert os.path.isfile(tmp_py)

git('rm', 'notebook.ipynb')
git('status')
git('commit', '-m', 'deleted')
git('status')

assert not os.path.isfile(tmp_ipynb)
assert not os.path.isfile(tmp_py)


def test_pre_commit_hook_py_to_ipynb_and_md(tmpdir):
tmp_ipynb = str(tmpdir.join('notebook.ipynb'))
tmp_py = str(tmpdir.join('notebook.py'))
tmp_md = str(tmpdir.join('notebook.md'))
nb = new_notebook(cells=[])

def git(*args):
print(system('git', *args, cwd=str(tmpdir)))

git('init')
git('status')
with open(str(tmpdir.join('.git/hooks/pre-commit')), 'w') as fp:
fp.write('#!/bin/sh\n'
'jupytext --from py:light --to ipynb --pre-commit\n'
'jupytext --from py:light --to md --pre-commit\n')

writef(nb, tmp_py)
assert os.path.isfile(tmp_py)
assert not os.path.isfile(tmp_ipynb)
assert not os.path.isfile(tmp_md)

git('add', 'notebook.py')
git('status')
git('commit', '-m', 'created')
git('status')

assert os.path.isfile(tmp_ipynb)
assert os.path.isfile(tmp_md)

git('rm', 'notebook.py')
git('status')
git('commit', '-m', 'deleted')
git('status')

assert not os.path.isfile(tmp_ipynb)
assert not os.path.isfile(tmp_py)
assert not os.path.isfile(tmp_md)

0 comments on commit 7fb93bb

Please sign in to comment.