https://github.com/python/cpython/commit/34d7351ac770ac49875fc39396b2a97828ba05ad
commit: 34d7351ac770ac49875fc39396b2a97828ba05ad
branch: main
author: Douglas Thor <[email protected]>
committer: hugovk <[email protected]>
date: 2025-08-08T18:34:02+03:00
summary:
gh-133722: Add Difflib theme to `_colorize` and 'color' option to
`difflib.unified_diff` (#133725)
files:
A Misc/NEWS.d/next/Library/2025-05-08-20-03-20.gh-issue-133722.1-B82a.rst
M Doc/library/difflib.rst
M Doc/whatsnew/3.15.rst
M Lib/_colorize.py
M Lib/difflib.py
M Lib/test/test_difflib.py
M Misc/ACKS
diff --git a/Doc/library/difflib.rst b/Doc/library/difflib.rst
index ec8b575a1ba999..c55ecac340972b 100644
--- a/Doc/library/difflib.rst
+++ b/Doc/library/difflib.rst
@@ -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
diff --git a/Doc/whatsnew/3.15.rst b/Doc/whatsnew/3.15.rst
index 93f56eed857068..9f01b52f1aff3b 100644
--- a/Doc/whatsnew/3.15.rst
+++ b/Doc/whatsnew/3.15.rst
@@ -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`.)
diff --git a/Lib/_colorize.py b/Lib/_colorize.py
index 4a310a402358b6..325efed274aed7 100644
--- a/Lib/_colorize.py
+++ b/Lib/_colorize.py
@@ -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 @@ def copy_with(
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 @@ def copy_with(
"""
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 @@ def no_colors(cls) -> Self:
"""
return cls(
argparse=Argparse.no_colors(),
+ difflib=Difflib.no_colors(),
syntax=Syntax.no_colors(),
traceback=Traceback.no_colors(),
unittest=Unittest.no_colors(),
diff --git a/Lib/difflib.py b/Lib/difflib.py
index 487936dbf47cdc..fedc85009aa03b 100644
--- a/Lib/difflib.py
+++ b/Lib/difflib.py
@@ -30,6 +30,7 @@
'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}'
########################################################################
diff --git a/Lib/test/test_difflib.py b/Lib/test/test_difflib.py
index 6ac584a08d1e86..0eab3f523dc5fe 100644
--- a/Lib/test/test_difflib.py
+++ b/Lib/test/test_difflib.py
@@ -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 @@ def test_range_format_context(self):
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
diff --git a/Misc/ACKS b/Misc/ACKS
index 745f472474cd9d..dc28ccf8f57eda 100644
--- a/Misc/ACKS
+++ b/Misc/ACKS
@@ -1902,6 +1902,7 @@ Nicolas M. ThiƩry
James Thomas
Reuben Thomas
Robin Thomas
+Douglas Thor
Brian Thorne
Christopher Thorne
Stephen Thorne
diff --git
a/Misc/NEWS.d/next/Library/2025-05-08-20-03-20.gh-issue-133722.1-B82a.rst
b/Misc/NEWS.d/next/Library/2025-05-08-20-03-20.gh-issue-133722.1-B82a.rst
new file mode 100644
index 00000000000000..86f244412498c4
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2025-05-08-20-03-20.gh-issue-133722.1-B82a.rst
@@ -0,0 +1,2 @@
+Added a *color* option to :func:`difflib.unified_diff` that colors output
+similar to :program:`git diff`.
_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]