diff --git a/nbdime/args.py b/nbdime/args.py index ceaa99b30..d1dc3f377 100644 --- a/nbdime/args.py +++ b/nbdime/args.py @@ -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 diff --git a/nbdime/nbdiffapp.py b/nbdime/nbdiffapp.py index ece783a14..13e98cbba 100644 --- a/nbdime/nbdiffapp.py +++ b/nbdime/nbdiffapp.py @@ -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: @@ -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( diff --git a/nbdime/tests/test_cli_apps.py b/nbdime/tests/test_cli_apps.py index 7cbe36c0c..a3305b5e4 100644 --- a/nbdime/tests/test_cli_apps.py +++ b/nbdime/tests/test_cli_apps.py @@ -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") diff --git a/nbdime/webapp/nbdiffwebexport.py b/nbdime/webapp/nbdiffwebexport.py new file mode 100644 index 000000000..2510637b5 --- /dev/null +++ b/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()) diff --git a/nbdime/webapp/nbdimeserver.py b/nbdime/webapp/nbdimeserver.py index 90fd5891f..1883dc4ad 100644 --- a/nbdime/webapp/nbdimeserver.py +++ b/nbdime/webapp/nbdimeserver.py @@ -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) diff --git a/nbdime/webapp/templates/diff.html b/nbdime/webapp/templates/diff.html index d077b67e9..4c3088c14 100644 --- a/nbdime/webapp/templates/diff.html +++ b/nbdime/webapp/templates/diff.html @@ -17,7 +17,7 @@

Notebook Diff

- + diff --git a/nbdime/webapp/templates/diffembedded.html b/nbdime/webapp/templates/diffembedded.html new file mode 100644 index 000000000..551fdde05 --- /dev/null +++ b/nbdime/webapp/templates/diffembedded.html @@ -0,0 +1,29 @@ +{% extends "nbdimepage.html" %} + + {% block nbdimeheader %} + +
+

Notebook Diff

+ +
+ + + +
+
+ {{ base_label }} + {{ remote_label }} +
+
+ + {% endblock %} + + {% block nbdimescripts %} + + {% if nbdime_js_url == "" %} + + {% else %} + + {% endif %} + + {% endblock %} diff --git a/nbdime/webapp/templates/difftool.html b/nbdime/webapp/templates/difftool.html index 8064884e9..299fbf27d 100644 --- a/nbdime/webapp/templates/difftool.html +++ b/nbdime/webapp/templates/difftool.html @@ -5,7 +5,7 @@

Notebook Diff

- + diff --git a/nbdime/webapp/templates/merge.html b/nbdime/webapp/templates/merge.html index 9de509773..6b80a34cb 100644 --- a/nbdime/webapp/templates/merge.html +++ b/nbdime/webapp/templates/merge.html @@ -20,7 +20,7 @@

Notebook Merge

- + diff --git a/nbdime/webapp/templates/mergetool.html b/nbdime/webapp/templates/mergetool.html index 7820add4a..f6e2d2634 100644 --- a/nbdime/webapp/templates/mergetool.html +++ b/nbdime/webapp/templates/mergetool.html @@ -5,7 +5,7 @@

Notebook Merge

- + diff --git a/nbdime/webapp/templates/nbdimepage.html b/nbdime/webapp/templates/nbdimepage.html index c209fd165..435ad0348 100644 --- a/nbdime/webapp/templates/nbdimepage.html +++ b/nbdime/webapp/templates/nbdimepage.html @@ -10,9 +10,7 @@ - - - + {% block nbdimeheader %} {% endblock %} @@ -20,8 +18,10 @@
+ {% block nbdimescripts %} + {% endblock %} diff --git a/packages/webapp/src/app/diff.ts b/packages/webapp/src/app/diff.ts index edc6a2dc6..6c09fe55f 100644 --- a/packages/webapp/src/app/diff.ts +++ b/packages/webapp/src/app/diff.ts @@ -17,10 +17,6 @@ import { defaultSanitizer } from '@jupyterlab/apputils'; -import { - PageConfig -} from '@jupyterlab/coreutils'; - import { MathJaxTypesetter } from '@jupyterlab/mathjax2'; @@ -158,13 +154,14 @@ function editHistory(pushHistory: boolean | 'replace', statedata: any, title: st export function getDiff(base: string, remote: string | undefined) { let baseUrl = getBaseUrl(); - requestDiff(base, remote, baseUrl, onDiffRequestCompleted, onDiffRequestFailed); + requestDiff(base, remote, baseUrl, renderDiff, renderError); } + /** * Callback for a successfull diff request */ -function onDiffRequestCompleted(data: any) { +function renderDiff(data: {base: nbformat.INotebookContent, diff: IDiffEntry[]}) { let layoutWork = showDiff(data); layoutWork.then(() => { @@ -178,13 +175,13 @@ function onDiffRequestCompleted(data: any) { /** * Callback for a failed diff request */ -function onDiffRequestFailed(response: string) { +function renderError(message: string) { console.log('Diff request failed.'); let root = document.getElementById('nbdime-root'); if (!root) { throw new Error('Missing root element "nbidme-root"'); } - root.innerHTML = '
' + response + '
'; + root.innerHTML = '
' + message + '
'; diffWidget = null; toggleSpinner(false); } @@ -242,12 +239,28 @@ function attachToForm() { */ export function initializeDiff() { - attachToForm(); - // If arguments supplied in config, run diff directly: - let base = getConfigOption('base'); - let remote = getConfigOption('remote'); - if (base && (remote || hasPrefix(base))) { - compare(base, remote, 'replace'); + // Check to see if we already have the diff data embedded + if (document.getElementById('diff-and-base')) { + let el = document.getElementById('diff-and-base'); + if (el && el.textContent) { + let data; + try { + data = JSON.parse(el.textContent); + } catch { + renderError('Failed to parse diff data'); + } + renderDiff(data); + } else { + renderError('Missing diff data'); + } + } else { + attachToForm(); + // If arguments supplied in config, run diff directly: + let base = getConfigOption('base'); + let remote = getConfigOption('remote'); + if (base && (remote || hasPrefix(base))) { + compare(base, remote, 'replace'); + } } let exportBtn = document.getElementById('nbdime-export') as HTMLButtonElement; diff --git a/setup.py b/setup.py index 73a73b749..def7ea8be 100644 --- a/setup.py +++ b/setup.py @@ -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',