Skip to content

Commit 34d7351

Browse files
authored
gh-133722: Add Difflib theme to _colorize and 'color' option to difflib.unified_diff (#133725)
1 parent 64ee1ba commit 34d7351

File tree

7 files changed

+73
-13
lines changed

7 files changed

+73
-13
lines changed

Doc/library/difflib.rst

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -278,7 +278,7 @@ diffs. For comparing directories and files, see also, the :mod:`filecmp` module.
278278
emu
279279

280280

281-
.. function:: unified_diff(a, b, fromfile='', tofile='', fromfiledate='', tofiledate='', n=3, lineterm='\n')
281+
.. function:: unified_diff(a, b, fromfile='', tofile='', fromfiledate='', tofiledate='', n=3, lineterm='\n', *, color=False)
282282

283283
Compare *a* and *b* (lists of strings); return a delta (a :term:`generator`
284284
generating the delta lines) in unified diff format.
@@ -297,6 +297,10 @@ diffs. For comparing directories and files, see also, the :mod:`filecmp` module.
297297
For inputs that do not have trailing newlines, set the *lineterm* argument to
298298
``""`` so that the output will be uniformly newline free.
299299

300+
Set *color* to ``True`` to enable output in color, similar to
301+
:program:`git diff --color`. Even if enabled, it can be
302+
:ref:`controlled using environment variables <using-on-controlling-color>`.
303+
300304
The unified diff format normally has a header for filenames and modification
301305
times. Any or all of these may be specified using strings for *fromfile*,
302306
*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.
319323

320324
See :ref:`difflib-interface` for a more detailed example.
321325

326+
.. versionchanged:: next
327+
Added the *color* parameter.
328+
329+
322330
.. function:: diff_bytes(dfunc, a, b, fromfile=b'', tofile=b'', fromfiledate=b'', tofiledate=b'', n=3, lineterm=b'\n')
323331

324332
Compare *a* and *b* (lists of bytes objects) using *dfunc*; yield a

Doc/whatsnew/3.15.rst

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,14 @@ dbm
229229
difflib
230230
-------
231231

232+
.. _whatsnew315-color-difflib:
233+
234+
* Introduced the optional *color* parameter to :func:`difflib.unified_diff`,
235+
enabling color output similar to :program:`git diff`.
236+
This can be controlled by :ref:`environment variables
237+
<using-on-controlling-color>`.
238+
(Contributed by Douglas Thor in :gh:`133725`.)
239+
232240
* Improved the styling of HTML diff pages generated by the :class:`difflib.HtmlDiff`
233241
class, and migrated the output to the HTML5 standard.
234242
(Contributed by Jiahao Li in :gh:`134580`.)

Lib/_colorize.py

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,18 @@ class Argparse(ThemeSection):
172172
reset: str = ANSIColors.RESET
173173

174174

175-
@dataclass(frozen=True)
175+
@dataclass(frozen=True, kw_only=True)
176+
class Difflib(ThemeSection):
177+
"""A 'git diff'-like theme for `difflib.unified_diff`."""
178+
added: str = ANSIColors.GREEN
179+
context: str = ANSIColors.RESET # context lines
180+
header: str = ANSIColors.BOLD # eg "---" and "+++" lines
181+
hunk: str = ANSIColors.CYAN # the "@@" lines
182+
removed: str = ANSIColors.RED
183+
reset: str = ANSIColors.RESET
184+
185+
186+
@dataclass(frozen=True, kw_only=True)
176187
class Syntax(ThemeSection):
177188
prompt: str = ANSIColors.BOLD_MAGENTA
178189
keyword: str = ANSIColors.BOLD_BLUE
@@ -186,7 +197,7 @@ class Syntax(ThemeSection):
186197
reset: str = ANSIColors.RESET
187198

188199

189-
@dataclass(frozen=True)
200+
@dataclass(frozen=True, kw_only=True)
190201
class Traceback(ThemeSection):
191202
type: str = ANSIColors.BOLD_MAGENTA
192203
message: str = ANSIColors.MAGENTA
@@ -198,7 +209,7 @@ class Traceback(ThemeSection):
198209
reset: str = ANSIColors.RESET
199210

200211

201-
@dataclass(frozen=True)
212+
@dataclass(frozen=True, kw_only=True)
202213
class Unittest(ThemeSection):
203214
passed: str = ANSIColors.GREEN
204215
warn: str = ANSIColors.YELLOW
@@ -207,14 +218,15 @@ class Unittest(ThemeSection):
207218
reset: str = ANSIColors.RESET
208219

209220

210-
@dataclass(frozen=True)
221+
@dataclass(frozen=True, kw_only=True)
211222
class Theme:
212223
"""A suite of themes for all sections of Python.
213224
214225
When adding a new one, remember to also modify `copy_with` and `no_colors`
215226
below.
216227
"""
217228
argparse: Argparse = field(default_factory=Argparse)
229+
difflib: Difflib = field(default_factory=Difflib)
218230
syntax: Syntax = field(default_factory=Syntax)
219231
traceback: Traceback = field(default_factory=Traceback)
220232
unittest: Unittest = field(default_factory=Unittest)
@@ -223,6 +235,7 @@ def copy_with(
223235
self,
224236
*,
225237
argparse: Argparse | None = None,
238+
difflib: Difflib | None = None,
226239
syntax: Syntax | None = None,
227240
traceback: Traceback | None = None,
228241
unittest: Unittest | None = None,
@@ -234,6 +247,7 @@ def copy_with(
234247
"""
235248
return type(self)(
236249
argparse=argparse or self.argparse,
250+
difflib=difflib or self.difflib,
237251
syntax=syntax or self.syntax,
238252
traceback=traceback or self.traceback,
239253
unittest=unittest or self.unittest,
@@ -249,6 +263,7 @@ def no_colors(cls) -> Self:
249263
"""
250264
return cls(
251265
argparse=Argparse.no_colors(),
266+
difflib=Difflib.no_colors(),
252267
syntax=Syntax.no_colors(),
253268
traceback=Traceback.no_colors(),
254269
unittest=Unittest.no_colors(),

Lib/difflib.py

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
'Differ','IS_CHARACTER_JUNK', 'IS_LINE_JUNK', 'context_diff',
3131
'unified_diff', 'diff_bytes', 'HtmlDiff', 'Match']
3232

33+
from _colorize import can_colorize, get_theme
3334
from heapq import nlargest as _nlargest
3435
from collections import namedtuple as _namedtuple
3536
from types import GenericAlias
@@ -1094,7 +1095,7 @@ def _format_range_unified(start, stop):
10941095
return '{},{}'.format(beginning, length)
10951096

10961097
def unified_diff(a, b, fromfile='', tofile='', fromfiledate='',
1097-
tofiledate='', n=3, lineterm='\n'):
1098+
tofiledate='', n=3, lineterm='\n', *, color=False):
10981099
r"""
10991100
Compare two sequences of lines; generate the delta as a unified diff.
11001101
@@ -1111,6 +1112,10 @@ def unified_diff(a, b, fromfile='', tofile='', fromfiledate='',
11111112
For inputs that do not have trailing newlines, set the lineterm
11121113
argument to "" so that the output will be uniformly newline free.
11131114
1115+
Set 'color' to True to enable output in color, similar to
1116+
'git diff --color'. Even if enabled, it can be
1117+
controlled using environment variables such as 'NO_COLOR'.
1118+
11141119
The unidiff format normally has a header for filenames and modification
11151120
times. Any or all of these may be specified using strings for
11161121
'fromfile', 'tofile', 'fromfiledate', and 'tofiledate'.
@@ -1134,32 +1139,37 @@ def unified_diff(a, b, fromfile='', tofile='', fromfiledate='',
11341139
four
11351140
"""
11361141

1142+
if color and can_colorize():
1143+
t = get_theme(force_color=True).difflib
1144+
else:
1145+
t = get_theme(force_no_color=True).difflib
1146+
11371147
_check_types(a, b, fromfile, tofile, fromfiledate, tofiledate, lineterm)
11381148
started = False
11391149
for group in SequenceMatcher(None,a,b).get_grouped_opcodes(n):
11401150
if not started:
11411151
started = True
11421152
fromdate = '\t{}'.format(fromfiledate) if fromfiledate else ''
11431153
todate = '\t{}'.format(tofiledate) if tofiledate else ''
1144-
yield '--- {}{}{}'.format(fromfile, fromdate, lineterm)
1145-
yield '+++ {}{}{}'.format(tofile, todate, lineterm)
1154+
yield f'{t.header}--- {fromfile}{fromdate}{lineterm}{t.reset}'
1155+
yield f'{t.header}+++ {tofile}{todate}{lineterm}{t.reset}'
11461156

11471157
first, last = group[0], group[-1]
11481158
file1_range = _format_range_unified(first[1], last[2])
11491159
file2_range = _format_range_unified(first[3], last[4])
1150-
yield '@@ -{} +{} @@{}'.format(file1_range, file2_range, lineterm)
1160+
yield f'{t.hunk}@@ -{file1_range} +{file2_range} @@{lineterm}{t.reset}'
11511161

11521162
for tag, i1, i2, j1, j2 in group:
11531163
if tag == 'equal':
11541164
for line in a[i1:i2]:
1155-
yield ' ' + line
1165+
yield f'{t.context} {line}{t.reset}'
11561166
continue
11571167
if tag in {'replace', 'delete'}:
11581168
for line in a[i1:i2]:
1159-
yield '-' + line
1169+
yield f'{t.removed}-{line}{t.reset}'
11601170
if tag in {'replace', 'insert'}:
11611171
for line in b[j1:j2]:
1162-
yield '+' + line
1172+
yield f'{t.added}+{line}{t.reset}'
11631173

11641174

11651175
########################################################################

Lib/test/test_difflib.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import difflib
2-
from test.support import findfile
2+
from test.support import findfile, force_colorized
33
import unittest
44
import doctest
55
import sys
@@ -355,6 +355,22 @@ def test_range_format_context(self):
355355
self.assertEqual(fmt(3,6), '4,6')
356356
self.assertEqual(fmt(0,0), '0')
357357

358+
@force_colorized
359+
def test_unified_diff_colored_output(self):
360+
args = [['one', 'three'], ['two', 'three'], 'Original', 'Current',
361+
'2005-01-26 23:30:50', '2010-04-02 10:20:52']
362+
actual = list(difflib.unified_diff(*args, lineterm='', color=True))
363+
364+
expect = [
365+
"\033[1m--- Original\t2005-01-26 23:30:50\033[0m",
366+
"\033[1m+++ Current\t2010-04-02 10:20:52\033[0m",
367+
"\033[36m@@ -1,2 +1,2 @@\033[0m",
368+
"\033[31m-one\033[0m",
369+
"\033[32m+two\033[0m",
370+
"\033[0m three\033[0m",
371+
]
372+
self.assertEqual(expect, actual)
373+
358374

359375
class TestBytes(unittest.TestCase):
360376
# don't really care about the content of the output, just the fact

Misc/ACKS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1902,6 +1902,7 @@ Nicolas M. Thiéry
19021902
James Thomas
19031903
Reuben Thomas
19041904
Robin Thomas
1905+
Douglas Thor
19051906
Brian Thorne
19061907
Christopher Thorne
19071908
Stephen Thorne
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Added a *color* option to :func:`difflib.unified_diff` that colors output
2+
similar to :program:`git diff`.

0 commit comments

Comments
 (0)