diff --git a/Doc/library/difflib.rst b/Doc/library/difflib.rst index ce948a6860f02c..0250ffd9f799d3 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 `. + 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 fe3d45b83a512e..5dd045a31b6aec 100644 --- a/Doc/whatsnew/3.15.rst +++ b/Doc/whatsnew/3.15.rst @@ -225,6 +225,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 + `. + (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 fabd79b9f74210..67ac91b231684e 100644 --- a/Misc/ACKS +++ b/Misc/ACKS @@ -1901,6 +1901,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`. 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