https://github.com/python/cpython/commit/bc977f450b328656e073b547f23aa201401023c7
commit: bc977f450b328656e073b547f23aa201401023c7
branch: main
author: Tomasz Kazimierczak <[email protected]>
committer: encukou <[email protected]>
date: 2026-09-04T14:20:30Z
summary:
gh-118150: difflib: expose autojunk flag from SequenceMatcher to public methods
and functions (GH-153959)
files:
A Misc/NEWS.d/next/Library/2026-07-18-16-43-02.gh-issue-118150.x0mV8P.rst
M Doc/library/difflib.rst
M Doc/whatsnew/3.16.rst
M Lib/difflib.py
M Lib/test/test_difflib.py
diff --git a/Doc/library/difflib.rst b/Doc/library/difflib.rst
index 5339186b72f6bc..583bf78d1649b6 100644
--- a/Doc/library/difflib.rst
+++ b/Doc/library/difflib.rst
@@ -128,7 +128,7 @@ Diff generation
The :class:`Differ` class has this constructor:
- .. method:: __init__(linejunk=None, charjunk=None)
+ .. method:: __init__(linejunk=None, charjunk=None, autojunk=True)
Optional keyword parameters *linejunk* and *charjunk* are for filter
functions
(or ``None``):
@@ -147,6 +147,14 @@ Diff generation
:meth:`~SequenceMatcher.find_longest_match` method's *isjunk*
parameter for an explanation.
+ Setting the optional *autojunk* argument to ``False`` will turn
+ :ref:`automatic junk heuristic <difflib-junk>` off.
+
+ .. versionchanged:: 3.16
+ Added keyword-only *autojunk* parameter.
+
+
+
:class:`Differ` objects are used (deltas generated) via a single method:
@@ -161,6 +169,8 @@ Diff generation
printed as-is via the :meth:`~io.IOBase.writelines` method of a
file-like object.
+
+
.. class:: HtmlDiff
This class can be used to create an HTML table (or a complete HTML file
@@ -176,7 +186,7 @@ Diff generation
The constructor for this class is:
- .. method:: __init__(tabsize=8, wrapcolumn=None, linejunk=None,
charjunk=IS_CHARACTER_JUNK)
+ .. method:: __init__(tabsize=8, wrapcolumn=None, linejunk=None,
charjunk=IS_CHARACTER_JUNK, *, autojunk=True)
Initializes instance of :class:`HtmlDiff`.
@@ -187,8 +197,15 @@ Diff generation
broken and wrapped, defaults to ``None`` where lines are not wrapped.
*linejunk* and *charjunk* are optional keyword arguments passed into
:func:`ndiff`
- (used by :class:`HtmlDiff` to generate the side by side HTML
differences). See
- :func:`ndiff` documentation for argument default values and descriptions.
+ (used by :class:`HtmlDiff` to generate the side by side HTML
differences).
+ See :func:`ndiff` documentation for argument default values and
descriptions.
+
+ Setting the optional *autojunk* argument to ``False`` will turn
+ :ref:`automatic junk heuristic <difflib-junk>` off.
+
+ .. versionchanged:: 3.16
+ Added keyword-only *autojunk* parameter.
+
The following methods are public:
@@ -231,7 +248,7 @@ Diff generation
-.. function:: context_diff(a, b, fromfile='', tofile='', fromfiledate='',
tofiledate='', n=3, lineterm='\n')
+.. function:: context_diff(a, b, fromfile='', tofile='', fromfiledate='',
tofiledate='', n=3, lineterm='\n', *, autojunk=True)
Compare *a* and *b* (lists of strings); return a delta (a :term:`generator`
generating the delta lines) in context diff format.
@@ -277,8 +294,14 @@ Diff generation
See :ref:`difflib-interface` for a more detailed example.
+ Setting the optional *autojunk* argument to ``False`` will turn
+ :ref:`automatic junk heuristic <difflib-junk>` off.
+
+ .. versionchanged:: 3.16
+ Added keyword-only *autojunk* parameter.
-.. function:: get_close_matches(word, possibilities, n=3, cutoff=0.6)
+
+.. function:: get_close_matches(word, possibilities, n=3, cutoff=0.6, *,
autojunk=True)
Return a list of the best "good enough" matches. *word* is a sequence for
which
close matches are desired (typically a string), and *possibilities* is a
list of
@@ -290,6 +313,9 @@ Diff generation
Optional argument *cutoff* (default ``0.6``) is a float in the range [0, 1].
Possibilities that don't score at least that similar to *word* are ignored.
+ Setting the optional *autojunk* argument to ``False`` will turn
+ :ref:`automatic junk heuristic <difflib-junk>` off.
+
The best (no more than *n*) matches among the possibilities are returned in
a
list, sorted by similarity score, most similar first.
@@ -303,8 +329,11 @@ Diff generation
>>> get_close_matches('accept', keyword.kwlist)
['except']
+ .. versionchanged:: 3.16
+ Added keyword-only *autojunk* parameter.
+
-.. function:: ndiff(a, b, linejunk=None, charjunk=IS_CHARACTER_JUNK)
+.. function:: ndiff(a, b, linejunk=None, charjunk=IS_CHARACTER_JUNK, *,
autojunk=True)
Compare *a* and *b* (lists of strings); return a :class:`Differ`\ -style
delta (a :term:`generator` generating the delta lines).
@@ -325,6 +354,11 @@ Diff generation
function :func:`IS_CHARACTER_JUNK`, which filters out whitespace characters
(a
blank or tab; it's a bad idea to include newline in this!).
+ Setting the optional *autojunk* argument to ``False`` will turn
+ :ref:`automatic junk heuristic <difflib-junk>` off.
+
+ Example:
+
>>> diff = ndiff('one\ntwo\nthree\n'.splitlines(keepends=True),
... 'ore\ntree\nemu\n'.splitlines(keepends=True))
>>> print(''.join(diff), end="")
@@ -338,6 +372,9 @@ Diff generation
+ tree
+ emu
+ .. versionchanged:: 3.16
+ Added keyword-only *autojunk* parameter.
+
.. function:: restore(sequence, which)
@@ -362,7 +399,7 @@ Diff generation
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', *, autojunk=True, color=False)
Compare *a* and *b* (lists of strings); return a delta (a :term:`generator`
generating the delta lines) in unified diff format.
@@ -410,6 +447,12 @@ Diff generation
.. versionchanged:: 3.15
Added the *color* parameter.
+ Setting the optional *autojunk* argument to ``False`` will turn
+ :ref:`automatic junk heuristic <difflib-junk>` off.
+
+ .. versionchanged:: 3.16
+ Added keyword-only *autojunk* parameter.
+
.. function:: diff_bytes(dfunc, a, b, fromfile=b'', tofile=b'',
fromfiledate=b'', tofiledate=b'', n=3, lineterm=b'\n')
diff --git a/Doc/whatsnew/3.16.rst b/Doc/whatsnew/3.16.rst
index 75541f1e106752..858dc3b8a878e7 100644
--- a/Doc/whatsnew/3.16.rst
+++ b/Doc/whatsnew/3.16.rst
@@ -304,6 +304,37 @@ curses
(Contributed by Serhiy Storchaka in :gh:`133031`.)
+ctypes
+------
+
+* Add :func:`ctypes.util.struct` for generating :class:`~ctypes.Structure`
types
+ from an annotation-based syntax, similar to how the :mod:`dataclasses` module
+ is used.
+ (Contributed by Peter Bierma in :gh:`104533`.)
+* Add :func:`ctypes.util.wrap_dll_function` for generating function pointers
+ through a function signature.
+ (Contributed by Peter Bierma in :gh:`153903`.)
+
+
+concurrent.futures
+------------------
+
+* The iterator returned by :meth:`concurrent.futures.Executor.map` is no longer
+ automatically closed if a function call raises an exception.
+ Use method :meth:`!close` to explicitly close the iterator.
+ (Contributed by xzmeng and Serhiy Storchaka in :gh:`108518`.)
+
+
+difflib
+-------
+
+* Expose optional ``autojunk`` parameter from :class:`difflib.SequenceMatcher`
+ to public functions and class methods in :mod:`difflib`,
+ allowing to modify behavior of automatic junk heuristic in this module
+ in higher public class methods and functions.
+ (Contributed by Tomasz Kazimierczak in :gh:`118150`)
+
+
encodings
---------
diff --git a/Lib/difflib.py b/Lib/difflib.py
index 95ba8fd782c6c3..c081cd8606df5b 100644
--- a/Lib/difflib.py
+++ b/Lib/difflib.py
@@ -664,7 +664,7 @@ def real_quick_ratio(self):
__class_getitem__ = classmethod(GenericAlias)
-def get_close_matches(word, possibilities, n=3, cutoff=0.6):
+def get_close_matches(word, possibilities, n=3, cutoff=0.6, *, autojunk=True):
"""Use SequenceMatcher to return list of the best "good enough" matches.
word is a sequence for which close matches are desired (typically a
@@ -698,7 +698,7 @@ def get_close_matches(word, possibilities, n=3, cutoff=0.6):
if not 0.0 <= cutoff <= 1.0:
raise ValueError("cutoff must be in [0.0, 1.0]: %r" % (cutoff,))
result = []
- s = SequenceMatcher()
+ s = SequenceMatcher(autojunk=autojunk)
s.set_seq2(word)
for x in possibilities:
s.set_seq1(x)
@@ -810,7 +810,7 @@ class Differ:
+ 5. Flat is better than nested.
"""
- def __init__(self, linejunk=None, charjunk=None):
+ def __init__(self, linejunk=None, charjunk=None, *, autojunk=True):
"""
Construct a text differencer, with optional filters.
@@ -828,10 +828,13 @@ def __init__(self, linejunk=None, charjunk=None):
module-level function `IS_CHARACTER_JUNK` may be used to filter out
whitespace characters (a blank or tab; **note**: bad idea to include
newline in this!). Use of IS_CHARACTER_JUNK is recommended.
+ - `autojunk`: automatic junk diff heuristic
+ (refer to :class:`SequenceMatcher` for specifics).
"""
self.linejunk = linejunk
self.charjunk = charjunk
+ self.autojunk = autojunk
def compare(self, a, b):
r"""
@@ -859,7 +862,7 @@ def compare(self, a, b):
+ emu
"""
- cruncher = SequenceMatcher(self.linejunk, a, b)
+ cruncher = SequenceMatcher(self.linejunk, a, b, autojunk=self.autojunk)
for tag, alo, ahi, blo, bhi in cruncher.get_opcodes():
if tag == 'replace':
g = self._fancy_replace(a, alo, ahi, b, blo, bhi)
@@ -920,7 +923,7 @@ def _fancy_replace(self, a, alo, ahi, b, blo, bhi):
# Later, more pathological cases prompted removing recursion
# entirely.
cutoff = 0.74999
- cruncher = SequenceMatcher(self.charjunk)
+ cruncher = SequenceMatcher(self.charjunk, autojunk=self.autojunk)
crqr = cruncher.real_quick_ratio
cqr = cruncher.quick_ratio
cr = cruncher.ratio
@@ -1099,7 +1102,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', *, autojunk=True,
color=False):
r"""
Compare two sequences of lines; generate the delta as a unified diff.
@@ -1120,6 +1123,9 @@ def unified_diff(a, b, fromfile='', tofile='',
fromfiledate='',
'git diff --color'. Even if enabled, it can be
controlled using environment variables such as 'NO_COLOR'.
+ Set `autojunk` to False if you don't want automated junk heuristic.
+ See details in :class:`SequenceMatcher.
+
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'.
@@ -1150,7 +1156,7 @@ def unified_diff(a, b, fromfile='', tofile='',
fromfiledate='',
_check_types(a, b, fromfile, tofile, fromfiledate, tofiledate, lineterm)
started = False
- for group in SequenceMatcher(None,a,b).get_grouped_opcodes(n):
+ for group in SequenceMatcher(None, a, b,
autojunk=autojunk).get_grouped_opcodes(n):
if not started:
started = True
fromdate = '\t{}'.format(fromfiledate) if fromfiledate else ''
@@ -1193,7 +1199,7 @@ def _format_range_context(start, stop):
# See http://www.unix.org/single_unix_specification/
def context_diff(a, b, fromfile='', tofile='',
- fromfiledate='', tofiledate='', n=3, lineterm='\n'):
+ fromfiledate='', tofiledate='', n=3, lineterm='\n', *,
autojunk=True):
r"""
Compare two sequences of lines; generate the delta as a context diff.
@@ -1216,6 +1222,10 @@ def context_diff(a, b, fromfile='', tofile='',
The modification times are normally expressed in the ISO 8601 format.
If not specified, the strings default to blanks.
+ The kwarg `autojunk` sets up automated junk heuristic with
+ :class:`SequenceMatcher`, which is used under the hood in this function.
+ See documentation of :class:`SequenceMatcher` for details.
+
Example:
>>> print(''.join(context_diff('one\ntwo\nthree\nfour\n'.splitlines(True),
@@ -1239,7 +1249,7 @@ def context_diff(a, b, fromfile='', tofile='',
_check_types(a, b, fromfile, tofile, fromfiledate, tofiledate, lineterm)
prefix = dict(insert='+ ', delete='- ', replace='! ', equal=' ')
started = False
- for group in SequenceMatcher(None,a,b).get_grouped_opcodes(n):
+ for group in SequenceMatcher(None, a, b,
autojunk=autojunk).get_grouped_opcodes(n):
if not started:
started = True
fromdate = '\t{}'.format(fromfiledate) if fromfiledate else ''
@@ -1321,7 +1331,7 @@ def decode(s):
for line in lines:
yield line.encode('ascii', 'surrogateescape')
-def ndiff(a, b, linejunk=None, charjunk=IS_CHARACTER_JUNK):
+def ndiff(a, b, linejunk=None, charjunk=IS_CHARACTER_JUNK, *, autojunk=True):
r"""
Compare `a` and `b` (lists of strings); return a `Differ`-style delta.
@@ -1339,6 +1349,8 @@ def ndiff(a, b, linejunk=None,
charjunk=IS_CHARACTER_JUNK):
whitespace characters (a blank or tab; note: it's a bad idea to
include newline in this!).
+ - autojunk: automatic junk heuristic - refer to :class:`SequenceMatcher`
for details
+
Tools/scripts/ndiff.py is a command-line front-end to this function.
Example:
@@ -1356,10 +1368,10 @@ def ndiff(a, b, linejunk=None,
charjunk=IS_CHARACTER_JUNK):
+ tree
+ emu
"""
- return Differ(linejunk, charjunk).compare(a, b)
+ return Differ(linejunk, charjunk, autojunk=autojunk).compare(a, b)
def _mdiff(fromlines, tolines, context=None, linejunk=None,
- charjunk=IS_CHARACTER_JUNK):
+ charjunk=IS_CHARACTER_JUNK, *, autojunk=True):
r"""Returns generator yielding marked up from/to side by side differences.
Arguments:
@@ -1369,6 +1381,7 @@ def _mdiff(fromlines, tolines, context=None,
linejunk=None,
if None, all from/to text lines will be generated.
linejunk -- passed on to ndiff (see ndiff documentation)
charjunk -- passed on to ndiff (see ndiff documentation)
+ autojunk -- passed on to ndiff (see ndiff documentation)
This function returns an iterator which returns a tuple:
(from line tuple, to line tuple, boolean flag)
@@ -1398,7 +1411,7 @@ def _mdiff(fromlines, tolines, context=None,
linejunk=None,
change_re = re.compile(r'(\++|\-+|\^+)')
# create the difference iterator to generate the differences
- diff_lines_iterator = ndiff(fromlines,tolines,linejunk,charjunk)
+ diff_lines_iterator = ndiff(fromlines, tolines, linejunk, charjunk,
autojunk=autojunk)
def _make_line(lines, format_key, side, num_lines=[0,0]):
"""Returns line of text with user's change markup and line formatting.
@@ -1738,14 +1751,14 @@ class HtmlDiff(object):
_default_prefix = 0
def __init__(self,tabsize=8,wrapcolumn=None,linejunk=None,
- charjunk=IS_CHARACTER_JUNK):
+ charjunk=IS_CHARACTER_JUNK, *, autojunk=True):
"""HtmlDiff instance initializer
Arguments:
tabsize -- tab stop spacing, defaults to 8.
wrapcolumn -- column number where lines are broken and wrapped,
defaults to None where lines are not wrapped.
- linejunk,charjunk -- keyword arguments passed into ndiff() (used by
+ linejunk, charjunk, autojunk -- keyword arguments passed into ndiff()
(used by
HtmlDiff() to generate the side by side HTML differences). See
ndiff() documentation for argument default values and descriptions.
"""
@@ -1753,6 +1766,7 @@ def __init__(self,tabsize=8,wrapcolumn=None,linejunk=None,
self._wrapcolumn = wrapcolumn
self._linejunk = linejunk
self._charjunk = charjunk
+ self._autojunk = autojunk
def make_file(self, fromlines, tolines, fromdesc='', todesc='',
context=False, numlines=5, *, charset='utf-8'):
@@ -2026,7 +2040,7 @@ def
make_table(self,fromlines,tolines,fromdesc='',todesc='',context=False,
else:
context_lines = None
diffs = _mdiff(fromlines,tolines,context_lines,linejunk=self._linejunk,
- charjunk=self._charjunk)
+ charjunk=self._charjunk, autojunk=self._autojunk)
# set up iterator to wrap lines that exceed desired width
if self._wrapcolumn:
diff --git a/Lib/test/test_difflib.py b/Lib/test/test_difflib.py
index 4f99b7c91c654e..5babe834e9beac 100644
--- a/Lib/test/test_difflib.py
+++ b/Lib/test/test_difflib.py
@@ -56,7 +56,7 @@ def test_bjunk(self):
class TestAutojunk(unittest.TestCase):
- """Tests for the autojunk parameter added in 2.7"""
+ """Tests for the autojunk parameter added in SequenceMatcher and
higher-level difflib APIs"""
def test_one_insert_homogenous_sequence(self):
# By default autojunk=True and the heuristic kicks in for a sequence
# of length 200+
@@ -72,6 +72,88 @@ def test_one_insert_homogenous_sequence(self):
self.assertAlmostEqual(sm.ratio(), 0.9975, places=3)
self.assertEqual(sm.bpopular, set())
+ def test_get_close_matches(self):
+ word = 'a' + 'b' * 200
+ possibilities = ['b' * 200]
+
+ # By default autojunk=True, so 'b' is junk -> ratio ~ 0 -> no matches
+ self.assertEqual(difflib.get_close_matches(word, possibilities,
cutoff=0.6), [])
+ self.assertEqual(difflib.get_close_matches(word, possibilities,
cutoff=0.6, autojunk=True), [])
+
+ # With autojunk=False, ratio ~ 0.9975 -> match returned
+ self.assertEqual(difflib.get_close_matches(word, possibilities,
cutoff=0.6, autojunk=False), ['b' * 200])
+
+ def test_differ_and_ndiff(self):
+ lines1 = ["x\n"] * 200 + ["a\n", "b\n", "c\n"] + ["x\n"] * 50
+ lines2 = ["a\n", "b\n", "c\n"] + ["x\n"] * 250
+
+ # Line-level autojunk propagation
+ d_true = difflib.Differ(autojunk=True)
+ d_false = difflib.Differ(autojunk=False)
+ res_true = list(d_true.compare(lines1, lines2))
+ res_false = list(d_false.compare(lines1, lines2))
+ self.assertNotEqual(res_true, res_false)
+
+ ndiff_true = list(difflib.ndiff(lines1, lines2, autojunk=True))
+ ndiff_false = list(difflib.ndiff(lines1, lines2, autojunk=False))
+ self.assertNotEqual(ndiff_true, ndiff_false)
+ self.assertEqual(ndiff_true, res_true)
+ self.assertEqual(ndiff_false, res_false)
+
+ # Character-level autojunk propagation in Differ (_fancy_replace)
+ line1 = "x" * 200 + "abc" + "x" * 50 + "\n"
+ line2 = "abc" + "x" * 250 + "\n"
+ fancy_true = list(difflib.Differ(autojunk=True).compare([line1],
[line2]))
+ fancy_false = list(difflib.Differ(autojunk=False).compare([line1],
[line2]))
+ self.assertNotEqual(fancy_true, fancy_false)
+
+ def test_unified_and_context_diff(self):
+ lines1 = ["x\n"] * 200 + ["a\n", "b\n", "c\n"] + ["x\n"] * 50
+ lines2 = ["a\n", "b\n", "c\n"] + ["x\n"] * 250
+
+ u_true = list(difflib.unified_diff(lines1, lines2, autojunk=True))
+ u_false = list(difflib.unified_diff(lines1, lines2, autojunk=False))
+ self.assertNotEqual(u_true, u_false)
+
+ c_true = list(difflib.context_diff(lines1, lines2, autojunk=True))
+ c_false = list(difflib.context_diff(lines1, lines2, autojunk=False))
+ self.assertNotEqual(c_true, c_false)
+
+ def test_htmldiff(self):
+ lines1 = ["x\n"] * 200 + ["a\n", "b\n", "c\n"] + ["x\n"] * 50
+ lines2 = ["a\n", "b\n", "c\n"] + ["x\n"] * 250
+
+ old_prefix = difflib.HtmlDiff._default_prefix
+ try:
+ html_true = difflib.HtmlDiff(autojunk=True).make_file(lines1,
lines2)
+ html_false = difflib.HtmlDiff(autojunk=False).make_file(lines1,
lines2)
+ self.assertNotEqual(html_true, html_false)
+ finally:
+ difflib.HtmlDiff._default_prefix = old_prefix
+
+ def test_autojunk_signatures(self):
+ import inspect
+
+ funcs = [
+ difflib.get_close_matches,
+ difflib.unified_diff,
+ difflib.context_diff,
+ difflib.ndiff,
+ ]
+ for func in funcs:
+ sig = inspect.signature(func)
+ self.assertIn('autojunk', sig.parameters)
+ param = sig.parameters['autojunk']
+ self.assertEqual(param.default, True)
+ self.assertEqual(param.kind, inspect.Parameter.KEYWORD_ONLY)
+
+ for cls in [difflib.Differ, difflib.HtmlDiff]:
+ sig = inspect.signature(cls.__init__)
+ self.assertIn('autojunk', sig.parameters)
+ param = sig.parameters['autojunk']
+ self.assertEqual(param.default, True)
+
+
class TestSFbugs(unittest.TestCase):
def test_ratio_for_null_seqn(self):
diff --git
a/Misc/NEWS.d/next/Library/2026-07-18-16-43-02.gh-issue-118150.x0mV8P.rst
b/Misc/NEWS.d/next/Library/2026-07-18-16-43-02.gh-issue-118150.x0mV8P.rst
new file mode 100644
index 00000000000000..b479302ade4064
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2026-07-18-16-43-02.gh-issue-118150.x0mV8P.rst
@@ -0,0 +1,5 @@
+Expose automated junk heuristic kwarg-only flag ``autojunk`` from
+:class:`difflib.SequenceMatcher` to the public functions
+and class methods in the :mod:`difflib`.
+See :class:`difflib.SequenceMatcher` documentation for details
+and issue :gh:`118150` for the motivation.
_______________________________________________
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]