diff -r c5464268aead Doc/library/difflib.rst --- a/Doc/library/difflib.rst Tue May 13 22:21:04 2014 -0700 +++ b/Doc/library/difflib.rst Fri May 16 14:30:13 2014 +0300 @@ -132,9 +132,6 @@ The arguments for this method are the same as those for the :meth:`make_file` method. - :file:`Tools/scripts/diff.py` is a command-line front-end to this class and - contains a good example of its use. - .. function:: context_diff(a, b, fromfile='', tofile='', fromfiledate='', tofiledate='', n=3, lineterm='\\n') @@ -716,72 +713,50 @@ .. _difflib-interface: -A command-line interface to difflib ------------------------------------ +Command line interface +---------------------- -This example shows how to use difflib to create a ``diff``-like utility. -It is also contained in the Python source distribution, as -:file:`Tools/scripts/diff.py`. +.. versionadded:: 3.5 -.. testcode:: +The :mod:`difflib` module provides a simple command line interface +which can be used as a ``diff``-like utility. - """ Command line interface to difflib.py providing diffs in four formats: +Command line options +^^^^^^^^^^^^^^^^^^^^ - * ndiff: lists every line and highlights interline changes. - * context: highlights clusters of changes in a before/after format. - * unified: highlights clusters of changes in an inline format. - * html: generates side by side comparison with change highlights. +.. cmdoption:: fromfile - """ + The first file used for comparison. - import sys, os, time, difflib, optparse +.. cmdoption:: tofile - def main(): - # Configure the option parser - usage = "usage: %prog [options] fromfile tofile" - parser = optparse.OptionParser(usage) - parser.add_option("-c", action="store_true", default=False, - help='Produce a context format diff (default)') - parser.add_option("-u", action="store_true", default=False, - help='Produce a unified format diff') - hlp = 'Produce HTML side by side diff (can use -c and -l in conjunction)' - parser.add_option("-m", action="store_true", default=False, help=hlp) - parser.add_option("-n", action="store_true", default=False, - help='Produce a ndiff format diff') - parser.add_option("-l", "--lines", type="int", default=3, - help='Set number of context lines (default 3)') - (options, args) = parser.parse_args() + The second file used for comparison. - if len(args) == 0: - parser.print_help() - sys.exit(1) - if len(args) != 2: - parser.error("need to specify both a fromfile and tofile") +.. cmdoption:: -c, --context - n = options.lines - fromfile, tofile = args # as specified in the usage string + Highlights clusters of changes in a before/after format. - # we're passing these as arguments to the diff function - fromdate = time.ctime(os.stat(fromfile).st_mtime) - todate = time.ctime(os.stat(tofile).st_mtime) - with open(fromfile) as fromf, open(tofile) as tof: - fromlines, tolines = list(fromf), list(tof) +.. cmdoption:: -u, --unified - if options.u: - diff = difflib.unified_diff(fromlines, tolines, fromfile, tofile, - fromdate, todate, n=n) - elif options.n: - diff = difflib.ndiff(fromlines, tolines) - elif options.m: - diff = difflib.HtmlDiff().make_file(fromlines, tolines, fromfile, - tofile, context=options.c, - numlines=n) - else: - diff = difflib.context_diff(fromlines, tolines, fromfile, tofile, - fromdate, todate, n=n) + Highlights clusters of changes in an inline format, + using :func:`unified_diff`. - # we're using writelines because diff is a generator - sys.stdout.writelines(diff) +.. cmdoption:: -m, --html - if __name__ == '__main__': - main() + Generates side by side comparison with change highlights. + +.. cmdoption:: -n, --ndiff + + Produce a *ndiff* format diff, by listing every line and + highlighting interline changes, using :func:`ndiff`. + +.. cmdoption:: -l, --lines + + Set number of context lines (defaults to ``3``). + +.. cmdoption:: -h, --help + + Show the help message. + +If none of the options ``-u``, ``-m``, ``-n``, ``-c`` will be specified, +the script will use :func:`unified_diff` as diffing function. diff -r c5464268aead Lib/difflib.py --- a/Lib/difflib.py Tue May 13 22:21:04 2014 -0700 +++ b/Lib/difflib.py Fri May 16 14:30:13 2014 +0300 @@ -2031,9 +2031,63 @@ if line[:2] in prefixes: yield line[2:] -def _test(): - import doctest, difflib - return doctest.testmod(difflib) + +def _main(): + import sys + import argparse + import os + from datetime import datetime, timezone + + def _file_mtime(path): + t = datetime.fromtimestamp(os.stat(path).st_mtime, + timezone.utc) + return t.astimezone().isoformat() + + parser = argparse.ArgumentParser() + parser.add_argument("fromfile", type=argparse.FileType('r'), + help="First input file for diffing.") + parser.add_argument("tofile", type=argparse.FileType('r'), + help="Second input file for diffing.") + parser.add_argument("-c", "--context", action="store_true", + default=False, + help='Produce a context format diff (default)') + parser.add_argument("-u", "--unified", action="store_true", + default=False, + help='Produce a unified format diff') + parser.add_argument("-m", "--html", action="store_true", + default=False, + help='Produce HTML side by side diff ' + '(can use -c and -l in conjunction)') + parser.add_argument("-n", "--ndiff", action="store_true", + default=False, help='Produce a ndiff format diff') + parser.add_argument("-l", "--lines", type=int, + default=3, + help='Set number of context lines (default 3)') + args = parser.parse_args() + n = args.lines + fromdate = _file_mtime(args.fromfile.name) + todate = _file_mtime(args.tofile.name) + fromlines = args.fromfile.readlines() + tolines = args.tofile.readlines() + with args.tofile, args.fromfile: + if args.context: + diff = context_diff(fromlines, tolines, + args.fromfile.name, args.tofile.name, + fromdate, todate, n=n) + elif args.ndiff: + diff = ndiff(fromlines, tolines) + elif args.html: + diff = HtmlDiff().make_file(fromlines, tolines, + args.fromfile.name, args.tofile.name, + context=args.context, numlines=n) + else: + diff = unified_diff(fromlines, tolines, + args.fromfile.name, args.tofile.name, + fromdate, todate, n=n) + + + sys.stdout.writelines(diff) + sys.exit(0) if __name__ == "__main__": - _test() + _main() diff -r c5464268aead Lib/test/test_difflib.py --- a/Lib/test/test_difflib.py Tue May 13 22:21:04 2014 -0700 +++ b/Lib/test/test_difflib.py Fri May 16 14:30:13 2014 +0300 @@ -3,6 +3,11 @@ import unittest import doctest import sys +import os +from contextlib import contextmanager +from datetime import datetime, timezone +from test.script_helper import assert_python_ok +from test.support import temp_dir, import_fresh_module class TestWithAscii(unittest.TestCase): @@ -277,13 +282,109 @@ self.assertEqual(fmt(3,6), '4,6') self.assertEqual(fmt(0,0), '0') +@contextmanager +def create_files(): + with temp_dir() as folder: + fromfile = os.path.join(folder, "fromfile") + tofile = os.path.join(folder, "tofile") + fromlines = os.linesep.join( + ["difflib spam", "fromfile tofile"]) + tolines = os.linesep.join( + ["difflib ham spam", "python fromfile tofile"]) + with open(fromfile, "w") as stream: + stream.write(fromlines) + with open(tofile, "w") as stream: + stream.write(tolines) + yield fromfile, tofile + +def file_mtime(path): + t = datetime.fromtimestamp(os.stat(path).st_mtime, + timezone.utc) + return t.astimezone().isoformat() + +def _get_diff_lines(fromfile, tofile): + with open(fromfile) as stream: + fromlines = stream.read().splitlines() + with open(tofile) as stream: + tolines = stream.read().splitlines() + fromdate = file_mtime(fromfile) + todate = file_mtime(tofile) + return (fromlines, tolines, fromdate, todate) + + +class TestCommandLine(unittest.TestCase): + + def assert_equal(self, output, expected): + output = output.decode() + for line in expected: + self.assertIn(line.strip(), output) + + def _ndiff(self, fromfile, tofile): + fromlines, tolines, _, _ = _get_diff_lines(fromfile, tofile) + return list(difflib.ndiff(fromlines, tolines)) + + def _unified(self, fromfile, tofile, n): + fromlines, tolines, fromdate, todate = _get_diff_lines(fromfile, tofile) + return list(difflib.unified_diff(fromlines, tolines, + fromfile, tofile, + fromdate, todate, + n=n)) + + def _context(self, fromfile, tofile, n): + fromlines, tolines, fromdate, todate = _get_diff_lines(fromfile, tofile) + return list(difflib.context_diff(fromlines, tolines, + fromfile, tofile, + fromdate, todate, + n=n)) + + def _html(self, fromfile, tofile, n): + # we need a fresh module because HtmlDiff uses a default prefix + # for table ids, which will make the comparison difficult + difflib = import_fresh_module('difflib') + fromlines, tolines, _, _ = _get_diff_lines(fromfile, tofile) + return difflib.HtmlDiff().make_file(fromlines, tolines, + fromfile, tofile, + context=False, numlines=n) + + def test_help(self): + rc, out, err = assert_python_ok('-m', 'difflib', '-h') + self.assertTrue(out.startswith(b'usage')) + self.assertEqual(err, b'') + self.assertEqual(rc, 0) + + def test_ndiff(self): + with create_files() as (fromfile, tofile): + rc, out, err = assert_python_ok('-m', 'difflib', fromfile, + tofile, '--ndiff') + self.assert_equal(out, self._ndiff(fromfile, tofile)) + + def test_unified(self): + with create_files() as (fromfile, tofile): + rc, out, err = assert_python_ok('-m', 'difflib', fromfile, + tofile, '--unified', '-l', '3') + self.assert_equal(out, self._unified(fromfile, tofile, 3)) + + def test_context_diff(self): + with create_files() as (fromfile, tofile): + rc, out, err = assert_python_ok('-m', 'difflib', fromfile, + tofile, '--context', '-l', '3') + self.assert_equal(out, self._context(fromfile, tofile, 3)) + + def test_context_diff(self): + with create_files() as (fromfile, tofile): + rc, out, err = assert_python_ok('-m', 'difflib', fromfile, + tofile, '--html', '-l', '3') + expected = self._html(fromfile, tofile, 3).splitlines() + out = out.decode().splitlines() + self.assertEqual(expected, out) + def test_main(): difflib.HtmlDiff._default_prefix = 0 Doctests = doctest.DocTestSuite(difflib) run_unittest( TestWithAscii, TestAutojunk, TestSFpatches, TestSFbugs, - TestOutputFormat, Doctests) + TestOutputFormat, Doctests, TestCommandLine) if __name__ == '__main__': test_main()