From 5c5b248402637b703c8272cd0a81b2f79d47383e Mon Sep 17 00:00:00 2001 From: Douglas Thor Date: Thu, 8 May 2025 19:51:57 -0700 Subject: [PATCH 01/22] Add test case --- Lib/test/test_difflib.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/Lib/test/test_difflib.py b/Lib/test/test_difflib.py index 9e217249be7332..56d6702b8e8fb6 100644 --- a/Lib/test/test_difflib.py +++ b/Lib/test/test_difflib.py @@ -355,6 +355,21 @@ def test_range_format_context(self): self.assertEqual(fmt(3,6), '4,6') self.assertEqual(fmt(0,0), '0') + 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[m", + "\033[1m+++ Current\t2010-04-02 10:20:52\033[m", + "\033[36m@@ -1,2 +1,2 @@\033[m", + "\033[31m-one\033[m", + "\033[32m+two\033[m", + " three", + ] + self.assertEqual(expect, actual) + class TestBytes(unittest.TestCase): # don't really care about the content of the output, just the fact From fdc0fa0d90ac0427456b987571745c8beed3b38c Mon Sep 17 00:00:00 2001 From: Douglas Thor Date: Thu, 8 May 2025 20:00:28 -0700 Subject: [PATCH 02/22] Add 'color' arg to difflib.unified_diff. Fixes gh-133722. --- Lib/difflib.py | 31 +++++++++++++++++++++++++------ 1 file changed, 25 insertions(+), 6 deletions(-) diff --git a/Lib/difflib.py b/Lib/difflib.py index f1f4e62514a7bd..e2fcc7bfbe69a2 100644 --- a/Lib/difflib.py +++ b/Lib/difflib.py @@ -1094,7 +1094,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 +1111,9 @@ 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 inject ANSI color codes and make the output look + like what `git diff --color` shows. + 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 +1137,15 @@ def unified_diff(a, b, fromfile='', tofile='', fromfiledate='', four """ + # {tag: ANSI color escape code} + colors = { + "delete": "\033[31m", # red + "insert": "\033[32m", # green + "header": "\033[1m", # bold / increased intensity + "hunk": "\033[36m", # cyan + } + reset = "\033[m" + _check_types(a, b, fromfile, tofile, fromfiledate, tofiledate, lineterm) started = False for group in SequenceMatcher(None,a,b).get_grouped_opcodes(n): @@ -1141,13 +1153,18 @@ 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) + _line = '--- {}{}{}'.format(fromfile, fromdate, lineterm) + yield colors["header"] + _line + reset if color else _line + _line = '+++ {}{}{}'.format(tofile, todate, lineterm) + yield colors["header"] + _line + reset if color else _line 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) + _line = '@@ -{} +{} @@{}'.format(file1_range, file2_range, lineterm) + if color: + _line = colors["hunk"] + _line + reset + yield _line for tag, i1, i2, j1, j2 in group: if tag == 'equal': @@ -1156,10 +1173,12 @@ def unified_diff(a, b, fromfile='', tofile='', fromfiledate='', continue if tag in {'replace', 'delete'}: for line in a[i1:i2]: - yield '-' + line + _line = '-' + line + yield colors["delete"] + _line + reset if color else _line if tag in {'replace', 'insert'}: for line in b[j1:j2]: - yield '+' + line + _line = '+' + line + yield colors["insert"] + _line + reset if color else _line ######################################################################## From fcdd7ab557c7a73d199f8d471879265d28aa15ea Mon Sep 17 00:00:00 2001 From: Douglas Thor Date: Thu, 8 May 2025 20:10:20 -0700 Subject: [PATCH 03/22] Update docs and ACKs --- Doc/library/difflib.rst | 9 ++++++++- Misc/ACKS | 1 + 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/Doc/library/difflib.rst b/Doc/library/difflib.rst index ce948a6860f02c..85fdaa0292d278 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,9 @@ 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 inject ANSI color codes and make the output look + like what ``git diff --color`` shows. + 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 +322,10 @@ diffs. For comparing directories and files, see also, the :mod:`filecmp` module. See :ref:`difflib-interface` for a more detailed example. + .. versionchanged:: 3.15 + 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/Misc/ACKS b/Misc/ACKS index 610dcf9f4238de..7f26e051a19f36 100644 --- a/Misc/ACKS +++ b/Misc/ACKS @@ -1889,6 +1889,7 @@ Nicolas M. ThiƩry James Thomas Reuben Thomas Robin Thomas +Douglas Thor Brian Thorne Christopher Thorne Stephen Thorne From 0e9b0706d5f2a7ecb392613fb264235edae2f932 Mon Sep 17 00:00:00 2001 From: Douglas Thor Date: Thu, 8 May 2025 20:12:04 -0700 Subject: [PATCH 04/22] blurb --- .../next/Library/2025-05-08-20-03-20.gh-issue-133722.1-B82a.rst | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2025-05-08-20-03-20.gh-issue-133722.1-B82a.rst 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..a6228918b8cf56 --- /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 injects ANSI color +codes to mimic ``git diff`` colors. From 7c3174974282c69e07493955be3f78d577e97d13 Mon Sep 17 00:00:00 2001 From: Douglas Thor Date: Thu, 8 May 2025 20:32:44 -0700 Subject: [PATCH 05/22] fixup to follow convention --- Lib/difflib.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Lib/difflib.py b/Lib/difflib.py index e2fcc7bfbe69a2..b0903250651ace 100644 --- a/Lib/difflib.py +++ b/Lib/difflib.py @@ -1162,9 +1162,7 @@ def unified_diff(a, b, fromfile='', tofile='', fromfiledate='', file1_range = _format_range_unified(first[1], last[2]) file2_range = _format_range_unified(first[3], last[4]) _line = '@@ -{} +{} @@{}'.format(file1_range, file2_range, lineterm) - if color: - _line = colors["hunk"] + _line + reset - yield _line + yield colors["hunk"] + _line + reset if color else _line for tag, i1, i2, j1, j2 in group: if tag == 'equal': From 66475a2c9fd65aa5ad3f24fd72053f93eb861425 Mon Sep 17 00:00:00 2001 From: Douglas Thor Date: Sat, 10 May 2025 20:51:13 -0700 Subject: [PATCH 06/22] Add 'Difflib' theme --- Lib/_colorize.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/Lib/_colorize.py b/Lib/_colorize.py index 4a310a402358b6..b1e2753448cc11 100644 --- a/Lib/_colorize.py +++ b/Lib/_colorize.py @@ -207,6 +207,16 @@ class Unittest(ThemeSection): reset: str = ANSIColors.RESET +@dataclass(frozen=True) +class Difflib(ThemeSection): + """A 'git diff'-like theme for `difflib.unified_diff`.""" + header: str = ANSIColors.BOLD # eg "---" and "+++" lines + hunk: str = ANSIColors.CYAN # the "@@" lines + insert: str = ANSIColors.GREEN + delete: str = ANSIColors.RED + reset: str = ANSIColors.RESET + + @dataclass(frozen=True) class Theme: """A suite of themes for all sections of Python. @@ -218,6 +228,7 @@ class Theme: syntax: Syntax = field(default_factory=Syntax) traceback: Traceback = field(default_factory=Traceback) unittest: Unittest = field(default_factory=Unittest) + difflib: Difflib = field(default_factory=Difflib) def copy_with( self, @@ -226,6 +237,7 @@ def copy_with( syntax: Syntax | None = None, traceback: Traceback | None = None, unittest: Unittest | None = None, + difflib: Difflib | None = None, ) -> Self: """Return a new Theme based on this instance with some sections replaced. @@ -237,6 +249,7 @@ def copy_with( syntax=syntax or self.syntax, traceback=traceback or self.traceback, unittest=unittest or self.unittest, + difflib=difflib or self.difflib, ) @classmethod @@ -252,6 +265,7 @@ def no_colors(cls) -> Self: syntax=Syntax.no_colors(), traceback=Traceback.no_colors(), unittest=Unittest.no_colors(), + difflib=Difflib.no_colors(), ) From dbf0547885e992d4ea2044fef2759f624dc94201 Mon Sep 17 00:00:00 2001 From: Douglas Thor Date: Sat, 10 May 2025 21:02:24 -0700 Subject: [PATCH 07/22] fixup tests --- Lib/test/test_difflib.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Lib/test/test_difflib.py b/Lib/test/test_difflib.py index 56d6702b8e8fb6..9f0428067cce55 100644 --- a/Lib/test/test_difflib.py +++ b/Lib/test/test_difflib.py @@ -361,11 +361,11 @@ def test_unified_diff_colored_output(self): actual = list(difflib.unified_diff(*args, lineterm='', color=True)) expect = [ - "\033[1m--- Original\t2005-01-26 23:30:50\033[m", - "\033[1m+++ Current\t2010-04-02 10:20:52\033[m", - "\033[36m@@ -1,2 +1,2 @@\033[m", - "\033[31m-one\033[m", - "\033[32m+two\033[m", + "\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", " three", ] self.assertEqual(expect, actual) From a72012e17f7da6de0156a9f684963b2623c99edf Mon Sep 17 00:00:00 2001 From: Douglas Thor Date: Sat, 10 May 2025 21:02:46 -0700 Subject: [PATCH 08/22] Switch to using themes. So easy! --- Lib/difflib.py | 28 ++++++++++------------------ 1 file changed, 10 insertions(+), 18 deletions(-) diff --git a/Lib/difflib.py b/Lib/difflib.py index b0903250651ace..30a5f128f0078c 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 @@ -1137,14 +1138,10 @@ def unified_diff(a, b, fromfile='', tofile='', fromfiledate='', four """ - # {tag: ANSI color escape code} - colors = { - "delete": "\033[31m", # red - "insert": "\033[32m", # green - "header": "\033[1m", # bold / increased intensity - "hunk": "\033[36m", # cyan - } - reset = "\033[m" + 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 @@ -1153,16 +1150,13 @@ def unified_diff(a, b, fromfile='', tofile='', fromfiledate='', started = True fromdate = '\t{}'.format(fromfiledate) if fromfiledate else '' todate = '\t{}'.format(tofiledate) if tofiledate else '' - _line = '--- {}{}{}'.format(fromfile, fromdate, lineterm) - yield colors["header"] + _line + reset if color else _line - _line = '+++ {}{}{}'.format(tofile, todate, lineterm) - yield colors["header"] + _line + reset if color else _line + yield '{}--- {}{}{}{}'.format(t.header, fromfile, fromdate, lineterm, t.reset) + yield '{}+++ {}{}{}{}'.format(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]) - _line = '@@ -{} +{} @@{}'.format(file1_range, file2_range, lineterm) - yield colors["hunk"] + _line + reset if color else _line + yield '{}@@ -{} +{} @@{}{}'.format(t.hunk, file1_range, file2_range, lineterm, t.reset) for tag, i1, i2, j1, j2 in group: if tag == 'equal': @@ -1171,12 +1165,10 @@ def unified_diff(a, b, fromfile='', tofile='', fromfiledate='', continue if tag in {'replace', 'delete'}: for line in a[i1:i2]: - _line = '-' + line - yield colors["delete"] + _line + reset if color else _line + yield f'{t.delete}-{line}{t.reset}' if tag in {'replace', 'insert'}: for line in b[j1:j2]: - _line = '+' + line - yield colors["insert"] + _line + reset if color else _line + yield f'{t.insert}+{line}{t.reset}' ######################################################################## From 2a3d818609974b374c617759ee45ecaac2e2e799 Mon Sep 17 00:00:00 2001 From: Douglas Thor Date: Sat, 10 May 2025 21:17:37 -0700 Subject: [PATCH 09/22] use 'next' in versionchanged docs --- Doc/library/difflib.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Doc/library/difflib.rst b/Doc/library/difflib.rst index 85fdaa0292d278..a870c45d8ad5e5 100644 --- a/Doc/library/difflib.rst +++ b/Doc/library/difflib.rst @@ -322,7 +322,7 @@ diffs. For comparing directories and files, see also, the :mod:`filecmp` module. See :ref:`difflib-interface` for a more detailed example. - .. versionchanged:: 3.15 + .. versionchanged:: next Added the *color* parameter. From 252982e3877c971bf5557ab382710cffbff121f3 Mon Sep 17 00:00:00 2001 From: Douglas Thor Date: Sat, 10 May 2025 21:34:39 -0700 Subject: [PATCH 10/22] turns out 'git diff' adds reset to the start and end of context lines --- Lib/_colorize.py | 1 + Lib/difflib.py | 2 +- Lib/test/test_difflib.py | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/Lib/_colorize.py b/Lib/_colorize.py index b1e2753448cc11..82e5cad72c0296 100644 --- a/Lib/_colorize.py +++ b/Lib/_colorize.py @@ -212,6 +212,7 @@ class Difflib(ThemeSection): """A 'git diff'-like theme for `difflib.unified_diff`.""" header: str = ANSIColors.BOLD # eg "---" and "+++" lines hunk: str = ANSIColors.CYAN # the "@@" lines + equal: str = ANSIColors.RESET # context lines insert: str = ANSIColors.GREEN delete: str = ANSIColors.RED reset: str = ANSIColors.RESET diff --git a/Lib/difflib.py b/Lib/difflib.py index 30a5f128f0078c..3746eaead45f02 100644 --- a/Lib/difflib.py +++ b/Lib/difflib.py @@ -1161,7 +1161,7 @@ def unified_diff(a, b, fromfile='', tofile='', fromfiledate='', for tag, i1, i2, j1, j2 in group: if tag == 'equal': for line in a[i1:i2]: - yield ' ' + line + yield f'{t.equal} {line}{t.reset}' continue if tag in {'replace', 'delete'}: for line in a[i1:i2]: diff --git a/Lib/test/test_difflib.py b/Lib/test/test_difflib.py index 9f0428067cce55..a6d4425120c274 100644 --- a/Lib/test/test_difflib.py +++ b/Lib/test/test_difflib.py @@ -366,7 +366,7 @@ def test_unified_diff_colored_output(self): "\033[36m@@ -1,2 +1,2 @@\033[0m", "\033[31m-one\033[0m", "\033[32m+two\033[0m", - " three", + "\033[0m three\033[0m", ] self.assertEqual(expect, actual) From 3422fa7ade56e41981f325b0859faf7b661557ca Mon Sep 17 00:00:00 2001 From: Douglas Thor Date: Tue, 13 May 2025 20:11:16 -0700 Subject: [PATCH 11/22] Use GNU unified diff terms --- Lib/_colorize.py | 6 +++--- Lib/difflib.py | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Lib/_colorize.py b/Lib/_colorize.py index 82e5cad72c0296..abd108749ef8f8 100644 --- a/Lib/_colorize.py +++ b/Lib/_colorize.py @@ -212,9 +212,9 @@ class Difflib(ThemeSection): """A 'git diff'-like theme for `difflib.unified_diff`.""" header: str = ANSIColors.BOLD # eg "---" and "+++" lines hunk: str = ANSIColors.CYAN # the "@@" lines - equal: str = ANSIColors.RESET # context lines - insert: str = ANSIColors.GREEN - delete: str = ANSIColors.RED + context: str = ANSIColors.RESET # context lines + added: str = ANSIColors.GREEN + removed: str = ANSIColors.RED reset: str = ANSIColors.RESET diff --git a/Lib/difflib.py b/Lib/difflib.py index 3746eaead45f02..eb422a6425dd6a 100644 --- a/Lib/difflib.py +++ b/Lib/difflib.py @@ -1161,14 +1161,14 @@ def unified_diff(a, b, fromfile='', tofile='', fromfiledate='', for tag, i1, i2, j1, j2 in group: if tag == 'equal': for line in a[i1:i2]: - yield f'{t.equal} {line}{t.reset}' + yield f'{t.context} {line}{t.reset}' continue if tag in {'replace', 'delete'}: for line in a[i1:i2]: - yield f'{t.delete}-{line}{t.reset}' + yield f'{t.removed}-{line}{t.reset}' if tag in {'replace', 'insert'}: for line in b[j1:j2]: - yield f'{t.insert}+{line}{t.reset}' + yield f'{t.added}+{line}{t.reset}' ######################################################################## From bffdd718d1948fe62273018e9c99f42889a75d5e Mon Sep 17 00:00:00 2001 From: Douglas Thor Date: Tue, 13 May 2025 20:13:12 -0700 Subject: [PATCH 12/22] move class --- Lib/_colorize.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/Lib/_colorize.py b/Lib/_colorize.py index abd108749ef8f8..245e31ca3154fe 100644 --- a/Lib/_colorize.py +++ b/Lib/_colorize.py @@ -172,6 +172,17 @@ class Argparse(ThemeSection): reset: str = ANSIColors.RESET +@dataclass(frozen=True) +class Difflib(ThemeSection): + """A 'git diff'-like theme for `difflib.unified_diff`.""" + header: str = ANSIColors.BOLD # eg "---" and "+++" lines + hunk: str = ANSIColors.CYAN # the "@@" lines + context: str = ANSIColors.RESET # context lines + added: str = ANSIColors.GREEN + removed: str = ANSIColors.RED + reset: str = ANSIColors.RESET + + @dataclass(frozen=True) class Syntax(ThemeSection): prompt: str = ANSIColors.BOLD_MAGENTA @@ -207,17 +218,6 @@ class Unittest(ThemeSection): reset: str = ANSIColors.RESET -@dataclass(frozen=True) -class Difflib(ThemeSection): - """A 'git diff'-like theme for `difflib.unified_diff`.""" - header: str = ANSIColors.BOLD # eg "---" and "+++" lines - hunk: str = ANSIColors.CYAN # the "@@" lines - context: str = ANSIColors.RESET # context lines - added: str = ANSIColors.GREEN - removed: str = ANSIColors.RED - reset: str = ANSIColors.RESET - - @dataclass(frozen=True) class Theme: """A suite of themes for all sections of Python. From 325586685c85bd492b92e4c2946bcc515cc91d34 Mon Sep 17 00:00:00 2001 From: Douglas Thor Date: Tue, 13 May 2025 20:16:29 -0700 Subject: [PATCH 13/22] kw-only the 'color' arg --- Lib/difflib.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/difflib.py b/Lib/difflib.py index eb422a6425dd6a..97cffcb4510d3b 100644 --- a/Lib/difflib.py +++ b/Lib/difflib.py @@ -1095,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', color=False): + tofiledate='', n=3, lineterm='\n', *, color=False): r""" Compare two sequences of lines; generate the delta as a unified diff. From c48a6ac443b7713e249e43723f47f6af90328cfc Mon Sep 17 00:00:00 2001 From: Douglas Thor Date: Tue, 13 May 2025 20:19:41 -0700 Subject: [PATCH 14/22] Doc formatting updates --- Doc/library/difflib.rst | 4 ++-- Lib/difflib.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Doc/library/difflib.rst b/Doc/library/difflib.rst index a870c45d8ad5e5..d32f5de44ece26 100644 --- a/Doc/library/difflib.rst +++ b/Doc/library/difflib.rst @@ -297,8 +297,8 @@ 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 inject ANSI color codes and make the output look - like what ``git diff --color`` shows. + Set *color* to ``True`` to inject ANSI color codes and make the output + look like what ``git diff --color`` shows. 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*, diff --git a/Lib/difflib.py b/Lib/difflib.py index 97cffcb4510d3b..f5fbf830628033 100644 --- a/Lib/difflib.py +++ b/Lib/difflib.py @@ -1112,8 +1112,8 @@ 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 inject ANSI color codes and make the output look - like what `git diff --color` shows. + Set *color* to ``True`` to inject ANSI color codes and make the output + look like what ``git diff --color`` shows. The unidiff format normally has a header for filenames and modification times. Any or all of these may be specified using strings for From 8ca50fa22b0d24ffa9a91ffe5759670ea05144bd Mon Sep 17 00:00:00 2001 From: Douglas Thor Date: Tue, 13 May 2025 20:21:05 -0700 Subject: [PATCH 15/22] Sort the things that are safe to sort without kw_only=True --- Lib/_colorize.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Lib/_colorize.py b/Lib/_colorize.py index 245e31ca3154fe..fb335b8e067483 100644 --- a/Lib/_colorize.py +++ b/Lib/_colorize.py @@ -175,10 +175,10 @@ class Argparse(ThemeSection): @dataclass(frozen=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 - context: str = ANSIColors.RESET # context lines - added: str = ANSIColors.GREEN removed: str = ANSIColors.RED reset: str = ANSIColors.RESET @@ -235,10 +235,10 @@ def copy_with( self, *, argparse: Argparse | None = None, + difflib: Difflib | None = None, syntax: Syntax | None = None, traceback: Traceback | None = None, unittest: Unittest | None = None, - difflib: Difflib | None = None, ) -> Self: """Return a new Theme based on this instance with some sections replaced. @@ -247,10 +247,10 @@ 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, - difflib=difflib or self.difflib, ) @classmethod @@ -263,10 +263,10 @@ 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(), - difflib=Difflib.no_colors(), ) From eb0e81e6dfd92f4e43c74c0173df5d2e7958cfcb Mon Sep 17 00:00:00 2001 From: Douglas Thor Date: Tue, 13 May 2025 20:36:27 -0700 Subject: [PATCH 16/22] Update what's new --- Doc/whatsnew/3.15.rst | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/Doc/whatsnew/3.15.rst b/Doc/whatsnew/3.15.rst index 7131eeb697eb69..3517f3406dcac9 100644 --- a/Doc/whatsnew/3.15.rst +++ b/Doc/whatsnew/3.15.rst @@ -65,6 +65,7 @@ Summary --- release highlights .. PEP-sized items next. +* :ref:`argparse `, New features @@ -86,10 +87,17 @@ New modules Improved modules ================ -module_name ------------ +difflib +------- + + .. _whatsnew314-color-argparse: + +* Introduced the optional *color* parameter to :func:`difflib.unified_diff`, + enabling colored output similar to what ``git diff`` displays. + This can be controlled by :ref:`environment variables + `. + (Contributed by Douglas Thor in :gh:`133725`.) -* TODO .. Add improved modules above alphabetically, not here at the end. From fb092b03d3b75a0948c76077896d0fefdf565080 Mon Sep 17 00:00:00 2001 From: Douglas Thor Date: Tue, 13 May 2025 20:58:24 -0700 Subject: [PATCH 17/22] fixup docs --- Doc/library/difflib.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Doc/library/difflib.rst b/Doc/library/difflib.rst index d32f5de44ece26..e49e4f2c7721a5 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', color=False) +.. 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. From 734b0bc0ba1f7a1ba155559c21ef16ef85312763 Mon Sep 17 00:00:00 2001 From: Douglas Thor Date: Mon, 19 May 2025 21:04:44 -0700 Subject: [PATCH 18/22] fixup docs --- Doc/library/difflib.rst | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Doc/library/difflib.rst b/Doc/library/difflib.rst index e49e4f2c7721a5..0250ffd9f799d3 100644 --- a/Doc/library/difflib.rst +++ b/Doc/library/difflib.rst @@ -297,8 +297,9 @@ 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 inject ANSI color codes and make the output - look like what ``git diff --color`` shows. + 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 `. 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*, From 8a10e4057eac8ebe526724685328f2fb855bbf64 Mon Sep 17 00:00:00 2001 From: Douglas Thor Date: Mon, 19 May 2025 21:05:26 -0700 Subject: [PATCH 19/22] Code review: docs, whatsnew, f-strings, news --- Doc/whatsnew/3.15.rst | 3 +-- Lib/difflib.py | 11 ++++++----- .../2025-05-08-20-03-20.gh-issue-133722.1-B82a.rst | 4 ++-- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/Doc/whatsnew/3.15.rst b/Doc/whatsnew/3.15.rst index cc49aca8aa2d8d..1ddb3ea2d9cebf 100644 --- a/Doc/whatsnew/3.15.rst +++ b/Doc/whatsnew/3.15.rst @@ -65,7 +65,6 @@ Summary --- release highlights .. PEP-sized items next. -* :ref:`argparse `, New features @@ -96,7 +95,7 @@ difflib .. _whatsnew315-color-difflib: * Introduced the optional *color* parameter to :func:`difflib.unified_diff`, - enabling colored output similar to what ``git diff`` displays. + enabling color output similar to :program:`git diff`. This can be controlled by :ref:`environment variables `. (Contributed by Douglas Thor in :gh:`133725`.) diff --git a/Lib/difflib.py b/Lib/difflib.py index f5fbf830628033..6a38104d154a0c 100644 --- a/Lib/difflib.py +++ b/Lib/difflib.py @@ -1112,8 +1112,9 @@ 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 inject ANSI color codes and make the output - look like what ``git diff --color`` shows. + 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 `. The unidiff format normally has a header for filenames and modification times. Any or all of these may be specified using strings for @@ -1150,13 +1151,13 @@ 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(t.header, fromfile, fromdate, lineterm, t.reset) - yield '{}+++ {}{}{}{}'.format(t.header, tofile, todate, lineterm, t.reset) + 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(t.hunk, file1_range, file2_range, lineterm, t.reset) + yield f'{t.hunk}@@ -{file1_range} +{file2_range} @@{lineterm}{t.reset}' for tag, i1, i2, j1, j2 in group: if tag == 'equal': 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 index a6228918b8cf56..86f244412498c4 100644 --- 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 @@ -1,2 +1,2 @@ -Added a ``color`` option to :func:`difflib.unified_diff` that injects ANSI color -codes to mimic ``git diff`` colors. +Added a *color* option to :func:`difflib.unified_diff` that colors output +similar to :program:`git diff`. From 57b80d1e15074b511f2bcaae05aff80a574b06d6 Mon Sep 17 00:00:00 2001 From: Douglas Thor Date: Mon, 19 May 2025 21:27:55 -0700 Subject: [PATCH 20/22] force_colorized --- Lib/test/test_difflib.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Lib/test/test_difflib.py b/Lib/test/test_difflib.py index a6d4425120c274..4371536f614079 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,7 @@ 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'] From c235425563a6624c76f4ef95ab7c2e9c9629230f Mon Sep 17 00:00:00 2001 From: Douglas Thor Date: Wed, 28 May 2025 21:56:38 -0700 Subject: [PATCH 21/22] kw_only --- Lib/_colorize.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Lib/_colorize.py b/Lib/_colorize.py index fb335b8e067483..325efed274aed7 100644 --- a/Lib/_colorize.py +++ b/Lib/_colorize.py @@ -172,7 +172,7 @@ 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 @@ -183,7 +183,7 @@ class Difflib(ThemeSection): reset: str = ANSIColors.RESET -@dataclass(frozen=True) +@dataclass(frozen=True, kw_only=True) class Syntax(ThemeSection): prompt: str = ANSIColors.BOLD_MAGENTA keyword: str = ANSIColors.BOLD_BLUE @@ -197,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 @@ -209,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 @@ -218,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. @@ -226,10 +226,10 @@ 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) - difflib: Difflib = field(default_factory=Difflib) def copy_with( self, From 833d86a4a5f0d999a1d42640220f44c0afdcdab9 Mon Sep 17 00:00:00 2001 From: Douglas Thor Date: Tue, 10 Jun 2025 21:12:53 -0700 Subject: [PATCH 22/22] documentation updates per code review --- Doc/whatsnew/3.15.rst | 1 + Lib/difflib.py | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/Doc/whatsnew/3.15.rst b/Doc/whatsnew/3.15.rst index 6d48a7c050f104..a4e46768dde008 100644 --- a/Doc/whatsnew/3.15.rst +++ b/Doc/whatsnew/3.15.rst @@ -99,6 +99,7 @@ difflib This can be controlled by :ref:`environment variables `. (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/difflib.py b/Lib/difflib.py index df1f0e9b43ea76..3b6893b7b7b83b 100644 --- a/Lib/difflib.py +++ b/Lib/difflib.py @@ -1112,9 +1112,9 @@ 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 - :program:`git diff --color`. Even if enabled, it can be - :ref:`controlled using environment variables `. + 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 pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy