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

Add nbdiff-web-export to export HTML diff using just command line #552

Open
wants to merge 7 commits into
base: master
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion nbdime/args.py
Expand Up @@ -369,7 +369,7 @@ def add_filter_args(diff_parser):


def add_git_diff_driver_args(diff_parser):
"""Adds a set of 7 stanard git diff driver arguments:
"""Adds a set of 7 standard git diff driver arguments:
path old-file old-hex old-mode new-file new-hex new-mode [ rename-to rename-metadata ]

Note: Only path, base and remote are added to parsed namespace
Expand Down
63 changes: 37 additions & 26 deletions nbdime/nbdiffapp.py
Expand Up @@ -33,39 +33,31 @@ def main_diff(args):
process_diff_flags(args)
base, remote, paths = resolve_diff_args(args)

# Check if base/remote are gitrefs:
# We are asked to do a diff of git revisions:
status = 0
for fbase, fremote in list_changed_file_pairs(base, remote, paths):
status = _handle_diff(fbase, fremote, output, args)
if status != 0:
# Short-circuit on error in diff handling
return status
return status


def list_changed_file_pairs(base, remote, paths):
if is_gitref(base) and is_gitref(remote):
# We are asked to do a diff of git revisions:
status = 0
for fbase, fremote in changed_notebooks(base, remote, paths):
status = _handle_diff(fbase, fremote, output, args)
if status != 0:
# Short-circuit on error in diff handling
return status
return status
yield fbase, fremote
else: # Not gitrefs:
return _handle_diff(base, remote, output, args)
yield base, remote


def _handle_diff(base, remote, output, args):
"""Handles diffs of files, either as filenames or file-like objects"""
# Check that if args are filenames they either exist, or are
# explicitly marked as missing (added/removed):
for fn in (base, remote):
if (isinstance(fn, string_types) and not os.path.exists(fn) and
fn != EXPLICIT_MISSING_FILE):
print("Missing file {}".format(fn))
return 1
# Both files cannot be missing
assert not (base == EXPLICIT_MISSING_FILE and remote == EXPLICIT_MISSING_FILE), (
'cannot diff %r against %r' % (base, remote))

# Perform actual work:
a = read_notebook(base, on_null='empty')
b = read_notebook(remote, on_null='empty')

d = diff_notebooks(a, b)

try:
a, b, d = _build_diff(base, remote, on_null="empty")
except ValueError as e:
print(e, file=sys.stderr)
return 1
# Output as JSON to file, or print to stdout:
if output:
with open(output, "w") as df:
Expand All @@ -90,6 +82,25 @@ def write(self, text):
return 0


def _build_diff(base, remote, on_null):
"""Builds diffs of files, either as filenames or file-like objects"""
# Check that if args are filenames they either exist, or are
# explicitly marked as missing (added/removed):
for fn in (base, remote):
if (isinstance(fn, string_types) and not os.path.exists(fn) and
fn != EXPLICIT_MISSING_FILE):
raise ValueError("Missing file {}".format(fn))
# Both files cannot be missing
assert not (base == EXPLICIT_MISSING_FILE and remote == EXPLICIT_MISSING_FILE), (
'cannot diff %r against %r' % (base, remote))

# Perform actual work:
base_notebook = read_notebook(base, on_null=on_null)
remote_notebook = read_notebook(remote, on_null=on_null)

d = diff_notebooks(base_notebook, remote_notebook)
return base_notebook, remote_notebook, d

def _build_arg_parser(prog=None):
"""Creates an argument parser for the nbdiff command."""
parser = ConfigBackedParser(
Expand Down
8 changes: 8 additions & 0 deletions nbdime/tests/test_cli_apps.py
Expand Up @@ -286,6 +286,14 @@ def test_nbdiff_app_no_colors(filespath, capsys):
assert 0 == main_diff(args)


def test_nbdiff_app_fail_if_file_is_missing(filespath):
afn = os.path.join(filespath, "missing-file.ipynb")
bfn = os.path.join(filespath, "multilevel-test-local.ipynb")

args = nbdiffapp._build_arg_parser().parse_args([afn, bfn])
assert 1 == main_diff(args)


def test_nbmerge_app(tempfiles, capsys, reset_log):
bfn = os.path.join(tempfiles, "multilevel-test-base.ipynb")
lfn = os.path.join(tempfiles, "multilevel-test-local.ipynb")
Expand Down
142 changes: 142 additions & 0 deletions nbdime/webapp/nbdiffwebexport.py
@@ -0,0 +1,142 @@
import sys
import os
import json

from jinja2 import FileSystemLoader, Environment

from ..args import (
Path,
ConfigBackedParser,
add_generic_args,
add_diff_args)

from ..nbdiffapp import (
list_changed_file_pairs,
resolve_diff_args,
_build_diff)


here = os.path.abspath(os.path.dirname(__file__))
static_path = os.path.join(here, 'static')
template_path = os.path.join(here, 'templates')


def build_arg_parser():
"""
Creates an argument parser for the web diff exporter.
"""
description = 'Export Nbdime diff.'
parser = ConfigBackedParser(
description=description,
add_help=True
)
add_generic_args(parser)
add_diff_args(parser)
parser.add_argument(
"base", help="the base notebook filename OR base git-revision.",
type=Path,
nargs='?', default='HEAD',
)
parser.add_argument(
"remote", help="the remote modified notebook filename OR remote git-revision.",
type=Path,
nargs='?', default=None,
)
parser.add_argument(
"paths", help="filter diffs for git-revisions based on path.",
type=Path,
nargs='*', default=None,
)
parser.add_argument(
"--nbdime-url",
default="",
help="URL to nbdime.js. If missing the script will be embedded in the HTML page."
)
parser.add_argument(
"--mathjax-url",
default="",
help="URL to MathJax JS. If blank, typsetting of LaTeX won't be available in the diff view."
)
parser.add_argument(
"--mathjax-config",
default="TeX-AMS_HTML-full,Safe",
help="config string for MathJax."
)
parser.add_argument(
'--show-unchanged',
dest='hide_unchanged',
action="store_false",
default=True,
help="show unchanged cells by default"
)
parser.add_argument(
"--output-dir",
type=Path,
default=".",
help="path to output directory."
)
return parser


def main_export(opts):

outputdir = opts.output_dir
os.makedirs(outputdir, exist_ok=True)

nbdime_content = ""
nbdime_url = opts.nbdime_url
if not nbdime_url:
# if nbdime_url is empty then we would embed the script
with open(os.path.join(static_path, "nbdime.js"), "r", encoding='utf8') as f:
nbdime_content = f.read()

base, remote, paths = resolve_diff_args(opts)

env = Environment(loader=FileSystemLoader([template_path]), autoescape=False)
index = 1
for fbase, fremote in list_changed_file_pairs(base, remote, paths):
# on_null="minimal" is crucial cause web renderer expects
# base_notebook to be a valid notebook even if it is missing
try:
base_notebook, remote_notebook, diff = _build_diff(fbase, fremote, on_null="minimal")
except ValueError as e:
print(e, file=sys.stderr)
return 1
data = dict(
base=base_notebook,
diff=diff
)

config = dict(
hideUnchange=opts.hide_unchanged,
mathjaxUrl=opts.mathjax_url,
mathjaxConfig=opts.mathjax_config,
)

# TODO: Create labels for use in template + filenames (instead of index)
template = env.get_template("diffembedded.html")
rendered = template.render(
data=data,
nbdime_js_url=nbdime_url,
nbdime_js_content=nbdime_content,
base_label='Base',
remote_label='Remote',
config_data=config,
)
outputfilename = os.path.join(outputdir, "nbdiff-" + str(index) + ".html")
with open(outputfilename, "w", encoding="utf8") as f:
f.write(rendered)
index += 1
print('Wrote %d diffs to %s' % (index - 1, outputdir))
return 0


def main(args=None):
if args is None:
args = sys.argv[1:]
opts = build_arg_parser().parse_args(args)
return main_export(opts)


if __name__ == "__main__":
sys.exit(main())
1 change: 1 addition & 0 deletions nbdime/webapp/nbdimeserver.py
Expand Up @@ -402,6 +402,7 @@ def make_app(**params):
app.exit_code = 0
return app


def init_app(on_port=None, closable=False, **params):
asyncio_patch()
_logger.debug('Using params: %s', params)
Expand Down
2 changes: 1 addition & 1 deletion nbdime/webapp/templates/diff.html
Expand Up @@ -17,7 +17,7 @@ <h3>Notebook Diff</h3>
</fieldset>
</form> <!-- nbdime-forms -->
<div id="nbdime-header-buttonrow">
<input id="nbdime-hide-unchanged" type="checkbox"><label for="cbox1">Hide unchanged cells</label></input>
<input id="nbdime-hide-unchanged" type="checkbox"><label for="nbdime-hide-unchanged">Hide unchanged cells</label></input>
<button id="nbdime-trust" style="display: none">Trust outputs</button>
<button id="nbdime-close" type="checkbox" style="display: none">Close tool</button>
<button id="nbdime-export" type="checkbox" style="display: none">Export diff</button>
Expand Down
29 changes: 29 additions & 0 deletions nbdime/webapp/templates/diffembedded.html
@@ -0,0 +1,29 @@
{% extends "nbdimepage.html" %}

{% block nbdimeheader %}

<div id="nbdime-header" class="nbdime-Diff">
<h3>Notebook Diff</h3>
<script id='diff-and-base' type="application/json">{{ data|tojson|safe }}</script>
<div id="nbdime-header-buttonrow">
<input id="nbdime-hide-unchanged" type="checkbox"><label for="nbdime-hide-unchanged">Hide unchanged cells</label></input>
<button id="nbdime-trust" style="display: none">Trust outputs</button>
<button id="nbdime-export" type="checkbox" style="display: none">Export diff</button>
</div>
<div id=nbdime-header-banner>
<span id="nbdime-header-base">{{ base_label }}</span>
<span id="nbdime-header-remote">{{ remote_label }}</span>
</div>
</div> <!-- ndime-header -->

{% endblock %}

{% block nbdimescripts %}
<script id='nbdime-config-data' type="application/json">{{ config_data|tojson|safe }}</script>
{% if nbdime_js_url == "" %}
<script>{{ nbdime_js_content }}</script>
{% else %}
<script src="{{ nbdime_js_url }}"></script>
{% endif %}
<noscript>Nbdime relies on javascript for diff/merge views!</noscript>
{% endblock %}
2 changes: 1 addition & 1 deletion nbdime/webapp/templates/difftool.html
Expand Up @@ -5,7 +5,7 @@
<div id="nbdime-header" class="nbdime-Diff">
<h3>Notebook Diff</h3>
<div id="nbdime-header-buttonrow">
<input id="nbdime-hide-unchanged" type="checkbox"><label for="cbox1">Hide unchanged cells</label></input>
<input id="nbdime-hide-unchanged" type="checkbox"><label for="nbdime-hide-unchanged">Hide unchanged cells</label></input>
<button id="nbdime-trust" style="display: none">Trust outputs</button>
<button id="nbdime-close" style="display: none">Close tool</button>
<button id="nbdime-export" style="display: none">Export diff</button>
Expand Down
2 changes: 1 addition & 1 deletion nbdime/webapp/templates/merge.html
Expand Up @@ -20,7 +20,7 @@ <h3>Notebook Merge</h3>
</fieldset>
</form> <!-- nbdime-forms -->
<div id="nbdime-header-buttonrow">
<input id="nbdime-hide-unchanged" type="checkbox"><label for="cbox1">Hide unchanged cells</label></input>
<input id="nbdime-hide-unchanged" type="checkbox"><label for="nbdime-hide-unchanged">Hide unchanged cells</label></input>
<button id="nbdime-save" style="display: none">Save</button>
<button id="nbdime-download" style="display: none">Download</button>
<button id="nbdime-close" style="display: none">Close tool</button>
Expand Down
2 changes: 1 addition & 1 deletion nbdime/webapp/templates/mergetool.html
Expand Up @@ -5,7 +5,7 @@
<div id="nbdime-header" class="nbdime-Merge">
<h3>Notebook Merge</h3>
<div id="nbdime-header-buttonrow">
<input id="nbdime-hide-unchanged" type="checkbox"><label for="cbox1">Hide unchanged cells</label></input>
<input id="nbdime-hide-unchanged" type="checkbox"><label for="nbdime-hide-unchanged">Hide unchanged cells</label></input>
<button id="nbdime-save" style="display: none">Save</button>
<button id="nbdime-download" style="display: none">Download</button>
<button id="nbdime-close" style="display: none">Close tool</button>
Expand Down
6 changes: 3 additions & 3 deletions nbdime/webapp/templates/nbdimepage.html
Expand Up @@ -10,18 +10,18 @@
</head>


<!-- TODO: make nbdime.init() setup the forms/input user interface? -->

<body> <!-- onload="nbdime.init()" -->
<body>

{% block nbdimeheader %}
{% endblock %}

<div id="nbdime-root" class="nbdime-root">
</div>

{% block nbdimescripts %}
<script id='nbdime-config-data' type="application/json">{{ config_data|tojson|safe }}</script>
<script src="{{ static_url('nbdime.js') }}"></script>
<noscript>Nbdime relies on javascript for diff/merge views!</noscript>
{% endblock %}
</body>
</html>