gh-133722: Add Difflib theme to _colorize and 'color' option to difflib.unified_diff (#133725)

This commit is contained in:
Douglas Thor 2025-08-08 08:34:02 -07:00 committed by GitHub
parent 64ee1babfb
commit 34d7351ac7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 73 additions and 13 deletions

View file

@ -278,7 +278,7 @@ diffs. For comparing directories and files, see also, the :mod:`filecmp` module.
emu
.. function:: unified_diff(a, b, fromfile='', tofile='', fromfiledate='', tofiledate='', n=3, lineterm='\n')
.. function:: unified_diff(a, b, fromfile='', tofile='', fromfiledate='', tofiledate='', n=3, lineterm='\n', *, color=False)
Compare *a* and *b* (lists of strings); return a delta (a :term:`generator`
generating the delta lines) in unified diff format.
@ -297,6 +297,10 @@ diffs. For comparing directories and files, see also, the :mod:`filecmp` module.
For inputs that do not have trailing newlines, set the *lineterm* argument to
``""`` so that the output will be uniformly newline free.
Set *color* to ``True`` to enable output in color, similar to
:program:`git diff --color`. Even if enabled, it can be
:ref:`controlled using environment variables <using-on-controlling-color>`.
The unified diff format normally has a header for filenames and modification
times. Any or all of these may be specified using strings for *fromfile*,
*tofile*, *fromfiledate*, and *tofiledate*. The modification times are normally
@ -319,6 +323,10 @@ diffs. For comparing directories and files, see also, the :mod:`filecmp` module.
See :ref:`difflib-interface` for a more detailed example.
.. versionchanged:: next
Added the *color* parameter.
.. function:: diff_bytes(dfunc, a, b, fromfile=b'', tofile=b'', fromfiledate=b'', tofiledate=b'', n=3, lineterm=b'\n')
Compare *a* and *b* (lists of bytes objects) using *dfunc*; yield a

View file

@ -229,6 +229,14 @@ dbm
difflib
-------
.. _whatsnew315-color-difflib:
* Introduced the optional *color* parameter to :func:`difflib.unified_diff`,
enabling color output similar to :program:`git diff`.
This can be controlled by :ref:`environment variables
<using-on-controlling-color>`.
(Contributed by Douglas Thor in :gh:`133725`.)
* Improved the styling of HTML diff pages generated by the :class:`difflib.HtmlDiff`
class, and migrated the output to the HTML5 standard.
(Contributed by Jiahao Li in :gh:`134580`.)

View file

@ -172,7 +172,18 @@ class Argparse(ThemeSection):
reset: str = ANSIColors.RESET
@dataclass(frozen=True)
@dataclass(frozen=True, kw_only=True)
class Difflib(ThemeSection):
"""A 'git diff'-like theme for `difflib.unified_diff`."""
added: str = ANSIColors.GREEN
context: str = ANSIColors.RESET # context lines
header: str = ANSIColors.BOLD # eg "---" and "+++" lines
hunk: str = ANSIColors.CYAN # the "@@" lines
removed: str = ANSIColors.RED
reset: str = ANSIColors.RESET
@dataclass(frozen=True, kw_only=True)
class Syntax(ThemeSection):
prompt: str = ANSIColors.BOLD_MAGENTA
keyword: str = ANSIColors.BOLD_BLUE
@ -186,7 +197,7 @@ class Syntax(ThemeSection):
reset: str = ANSIColors.RESET
@dataclass(frozen=True)
@dataclass(frozen=True, kw_only=True)
class Traceback(ThemeSection):
type: str = ANSIColors.BOLD_MAGENTA
message: str = ANSIColors.MAGENTA
@ -198,7 +209,7 @@ class Traceback(ThemeSection):
reset: str = ANSIColors.RESET
@dataclass(frozen=True)
@dataclass(frozen=True, kw_only=True)
class Unittest(ThemeSection):
passed: str = ANSIColors.GREEN
warn: str = ANSIColors.YELLOW
@ -207,7 +218,7 @@ class Unittest(ThemeSection):
reset: str = ANSIColors.RESET
@dataclass(frozen=True)
@dataclass(frozen=True, kw_only=True)
class Theme:
"""A suite of themes for all sections of Python.
@ -215,6 +226,7 @@ class Theme:
below.
"""
argparse: Argparse = field(default_factory=Argparse)
difflib: Difflib = field(default_factory=Difflib)
syntax: Syntax = field(default_factory=Syntax)
traceback: Traceback = field(default_factory=Traceback)
unittest: Unittest = field(default_factory=Unittest)
@ -223,6 +235,7 @@ class Theme:
self,
*,
argparse: Argparse | None = None,
difflib: Difflib | None = None,
syntax: Syntax | None = None,
traceback: Traceback | None = None,
unittest: Unittest | None = None,
@ -234,6 +247,7 @@ class Theme:
"""
return type(self)(
argparse=argparse or self.argparse,
difflib=difflib or self.difflib,
syntax=syntax or self.syntax,
traceback=traceback or self.traceback,
unittest=unittest or self.unittest,
@ -249,6 +263,7 @@ class Theme:
"""
return cls(
argparse=Argparse.no_colors(),
difflib=Difflib.no_colors(),
syntax=Syntax.no_colors(),
traceback=Traceback.no_colors(),
unittest=Unittest.no_colors(),

View file

@ -30,6 +30,7 @@ __all__ = ['get_close_matches', 'ndiff', 'restore', 'SequenceMatcher',
'Differ','IS_CHARACTER_JUNK', 'IS_LINE_JUNK', 'context_diff',
'unified_diff', 'diff_bytes', 'HtmlDiff', 'Match']
from _colorize import can_colorize, get_theme
from heapq import nlargest as _nlargest
from collections import namedtuple as _namedtuple
from types import GenericAlias
@ -1094,7 +1095,7 @@ def _format_range_unified(start, stop):
return '{},{}'.format(beginning, length)
def unified_diff(a, b, fromfile='', tofile='', fromfiledate='',
tofiledate='', n=3, lineterm='\n'):
tofiledate='', n=3, lineterm='\n', *, color=False):
r"""
Compare two sequences of lines; generate the delta as a unified diff.
@ -1111,6 +1112,10 @@ def unified_diff(a, b, fromfile='', tofile='', fromfiledate='',
For inputs that do not have trailing newlines, set the lineterm
argument to "" so that the output will be uniformly newline free.
Set 'color' to True to enable output in color, similar to
'git diff --color'. Even if enabled, it can be
controlled using environment variables such as 'NO_COLOR'.
The unidiff format normally has a header for filenames and modification
times. Any or all of these may be specified using strings for
'fromfile', 'tofile', 'fromfiledate', and 'tofiledate'.
@ -1134,6 +1139,11 @@ def unified_diff(a, b, fromfile='', tofile='', fromfiledate='',
four
"""
if color and can_colorize():
t = get_theme(force_color=True).difflib
else:
t = get_theme(force_no_color=True).difflib
_check_types(a, b, fromfile, tofile, fromfiledate, tofiledate, lineterm)
started = False
for group in SequenceMatcher(None,a,b).get_grouped_opcodes(n):
@ -1141,25 +1151,25 @@ def unified_diff(a, b, fromfile='', tofile='', fromfiledate='',
started = True
fromdate = '\t{}'.format(fromfiledate) if fromfiledate else ''
todate = '\t{}'.format(tofiledate) if tofiledate else ''
yield '--- {}{}{}'.format(fromfile, fromdate, lineterm)
yield '+++ {}{}{}'.format(tofile, todate, lineterm)
yield f'{t.header}--- {fromfile}{fromdate}{lineterm}{t.reset}'
yield f'{t.header}+++ {tofile}{todate}{lineterm}{t.reset}'
first, last = group[0], group[-1]
file1_range = _format_range_unified(first[1], last[2])
file2_range = _format_range_unified(first[3], last[4])
yield '@@ -{} +{} @@{}'.format(file1_range, file2_range, lineterm)
yield f'{t.hunk}@@ -{file1_range} +{file2_range} @@{lineterm}{t.reset}'
for tag, i1, i2, j1, j2 in group:
if tag == 'equal':
for line in a[i1:i2]:
yield ' ' + line
yield f'{t.context} {line}{t.reset}'
continue
if tag in {'replace', 'delete'}:
for line in a[i1:i2]:
yield '-' + line
yield f'{t.removed}-{line}{t.reset}'
if tag in {'replace', 'insert'}:
for line in b[j1:j2]:
yield '+' + line
yield f'{t.added}+{line}{t.reset}'
########################################################################

View file

@ -1,5 +1,5 @@
import difflib
from test.support import findfile
from test.support import findfile, force_colorized
import unittest
import doctest
import sys
@ -355,6 +355,22 @@ class TestOutputFormat(unittest.TestCase):
self.assertEqual(fmt(3,6), '4,6')
self.assertEqual(fmt(0,0), '0')
@force_colorized
def test_unified_diff_colored_output(self):
args = [['one', 'three'], ['two', 'three'], 'Original', 'Current',
'2005-01-26 23:30:50', '2010-04-02 10:20:52']
actual = list(difflib.unified_diff(*args, lineterm='', color=True))
expect = [
"\033[1m--- Original\t2005-01-26 23:30:50\033[0m",
"\033[1m+++ Current\t2010-04-02 10:20:52\033[0m",
"\033[36m@@ -1,2 +1,2 @@\033[0m",
"\033[31m-one\033[0m",
"\033[32m+two\033[0m",
"\033[0m three\033[0m",
]
self.assertEqual(expect, actual)
class TestBytes(unittest.TestCase):
# don't really care about the content of the output, just the fact

View file

@ -1902,6 +1902,7 @@ Nicolas M. Thiéry
James Thomas
Reuben Thomas
Robin Thomas
Douglas Thor
Brian Thorne
Christopher Thorne
Stephen Thorne

View file

@ -0,0 +1,2 @@
Added a *color* option to :func:`difflib.unified_diff` that colors output
similar to :program:`git diff`.