You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
SequenceMatcher.get_grouped_opcodes() changes the result of later get_opcodes() calls #161973
SequenceMatcher.get_opcodes() caches its result and returns the same list on every call. get_grouped_opcodes() takes that list and overwrites its first and last entries to trim the context, so after one call the matcher's opcodes no longer cover the whole input, and a later get_grouped_opcodes() with a larger n shows less context than it should.
This was mentioned in gh-54194 back in 2010, but that issue was closed after only the get_matching_blocks() part was fixed (gh-65834); the get_grouped_opcodes() part is still there.
importdifflibold= ["line %d\n"%iforiinrange(10)]
new=old[:]
new[5] ="changed\n"sm=difflib.SequenceMatcher(None, old, new)
forgroupinsm.get_grouped_opcodes(1):
pass# show the hunks with one line of contextprint(sm.get_opcodes())
I confirmed the alias in current Lib/difflib.py: get_opcodes() returns self.opcodes directly, then the get_grouped_opcodes() generator binds codes to that same list and replaces codes[0]/codes[-1] while trimming equal spans. Copying before those fixups (codes = self.get_opcodes().copy()) should isolate the mutation; the opcode tuples themselves are immutable.
For regression coverage, consume list(sm.get_grouped_opcodes(1)), then assert sm.get_opcodes() still equals the full original opcodes. On the same matcher, compare get_grouped_opcodes(3) with a fresh matcher so a later request for wider context cannot inherit the earlier trimming.
I'd like to take this on. The bug is clear from the report: get_opcodes() returns the cached self.opcodes list, and get_grouped_opcodes() mutates its first and last entries while trimming context, so a later get_opcodes() or a wider n sees the trimmed list instead of the full one. I'm working on a fix that copies the list before trimming, plus a regression test.
Activity
brunnojob commentedon Nov 14, 2026
I confirmed the alias in current
Lib/difflib.py:get_opcodes()returnsself.opcodesdirectly, then theget_grouped_opcodes()generator bindscodesto that same list and replacescodes[0]/codes[-1]while trimming equal spans. Copying before those fixups (codes = self.get_opcodes().copy()) should isolate the mutation; the opcode tuples themselves are immutable.For regression coverage, consume
list(sm.get_grouped_opcodes(1)), then assertsm.get_opcodes()still equals the full original opcodes. On the same matcher, compareget_grouped_opcodes(3)with a fresh matcher so a later request for wider context cannot inherit the earlier trimming.tanveer-arch commentedon Nov 14, 2026
I'd like to take this on. The bug is clear from the report: get_opcodes() returns the cached self.opcodes list, and get_grouped_opcodes() mutates its first and last entries while trimming context, so a later get_opcodes() or a wider n sees the trimmed list instead of the full one. I'm working on a fix that copies the list before trimming, plus a regression test.
Broyojo commentedon Nov 14, 2026
Thanks for confirming! I had a fix with a regression test ready, so I've opened #162012.