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 3 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
10 changes: 10 additions & 0 deletions nbdime/tests/test_cli_apps.py
Expand Up @@ -286,6 +286,16 @@ def test_nbdiff_app_no_colors(filespath, capsys):
assert 0 == main_diff(args)


def test_nbdiff_app_fail_if_file_is_missing(filespath):
# Simply check that the --color-words argument is accepted, not behavior
Copy link
Collaborator

Choose a reason for hiding this comment

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

?

Copy link
Author

Choose a reason for hiding this comment

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

Good catch. I just copy pasted the test with a comment

# Behavior is covered elsewhere
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
107 changes: 107 additions & 0 deletions nbdime/webapp/nbdiffwebexport.py
@@ -0,0 +1,107 @@
import sys
import os
import json

from jinja2 import FileSystemLoader, Environment

from ..args import (
Path,
ConfigBackedParser,
add_generic_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 diff tool, that also lets the
user specify a port and displays a help message.
"""
description = 'Difftool for Nbdime.'
parser = ConfigBackedParser(
description=description,
add_help=True
)
add_generic_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",
type=Path,
default="",
help="URL to nbdime.js"
)
parser.add_argument(
"--output-dir",
type=Path,
default="output/",
help="a path to an output dir"
)
return parser


def main_export(opts):
env = Environment(loader=FileSystemLoader([template_path]), autoescape=False)
outputdir = opts.output_dir
nbdime_url = opts.nbdime_url
if not nbdime_url:
nbdime_url = "nbdime.js"
import shutil
shutil.copy(os.path.join(static_path, "nbdime.js"), os.path.join(outputdir, nbdime_url))

base, remote, paths = resolve_diff_args(opts)
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 = json.dumps(dict(
base=base_notebook,
diff=diff
))

template = env.get_template("diffembedded.html")
rendered = template.render(
data=data,
nbdime_url=nbdime_url)
outputfilename = os.path.join(outputdir, "diff" + str(index) + ".html")
with open(outputfilename, "w") as f:
f.write(rendered)
index += 1
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
38 changes: 38 additions & 0 deletions nbdime/webapp/templates/diffembedded.html
@@ -0,0 +1,38 @@
<!DOCTYPE html>
<html>

<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />

<title>nbdime - diff and merge your Jupyter notebooks</title>

</head>


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

<body>

<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="cbox1">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>
</div>
<div id=nbdime-header-banner>
<span id="nbdime-header-base">Base</span>
<span id="nbdime-header-remote">Remote</span>
</div>
</div> <!-- ndime-header -->

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

<script src="{{ nbdime_url }}"></script>
vidartf marked this conversation as resolved.
Show resolved Hide resolved
<noscript>Nbdime relies on javascript for diff/merge views!</noscript>
</body>
</html>
5 changes: 5 additions & 0 deletions packages/webapp/src/app/diff.ts
Expand Up @@ -161,6 +161,11 @@ function getDiff(base: string, remote: string | undefined) {
requestDiff(base, remote, baseUrl, onDiffRequestCompleted, onDiffRequestFailed);
}

export
function renderDiff(diff: any) {
onDiffRequestCompleted(JSON.parse(diff));
}

/**
* Callback for a successfull diff request
*/
Expand Down
13 changes: 10 additions & 3 deletions packages/webapp/src/index.ts
Expand Up @@ -3,7 +3,7 @@
'use strict';

import {
initializeDiff
initializeDiff, renderDiff
} from './app/diff';

import {
Expand Down Expand Up @@ -44,8 +44,15 @@ import './app/merge.css';
/** */
function initialize() {
let closable = getConfigOption('closable');
let type: 'diff' | 'merge' | 'compare';
if (document.getElementById('compare-local')) {
let type: 'diff' | 'merge' | 'compare' | 'diff-and-base';
if (document.getElementById('diff-and-base')) {
type = 'diff-and-base'
closable = false
let el = document.getElementById('diff-and-base');
if (el && el.textContent) {
renderDiff(JSON.parse(el.textContent))
}
} else if (document.getElementById('compare-local')) {
initializeCompare();
type = 'compare';
} else if (getConfigOption('local') || document.getElementById('merge-local')) {
Expand Down
1 change: 1 addition & 0 deletions setup.py
Expand Up @@ -145,6 +145,7 @@
'nbshow = nbdime.nbshowapp:main',
'nbdiff = nbdime.nbdiffapp:main',
'nbdiff-web = nbdime.webapp.nbdiffweb:main',
'nbdiff-web-export = nbdime.webapp.nbdiffwebexport:main',
'nbmerge = nbdime.nbmergeapp:main',
'nbmerge-web = nbdime.webapp.nbmergeweb:main',
'git-nbdiffdriver = nbdime.vcs.git.diffdriver:main',
Expand Down