Accept single-edit typos in correction gates

difflib's SequenceMatcher underrates adjacent transpositions, so the
flagship examples (gti->git 0.667, greo->grep 0.75, psuh->push 0.75)
fell below the 0.8 ratio gates and were declined. Per the plan
amendment, the token and candidate gates in the history resolver,
guess_from_path and the help resolver additionally accept a
first-char-equal single edit (one substitution, insertion, deletion
or adjacent transposition; new thefuck/typo.py), with the ratio
cutoffs, the exact-one-candidate rule and the 0.79 boundary (edit
distance >= 2) unchanged.
This commit is contained in:
Alexander
2026-09-13 21:00:20 +02:00
parent 572d301bfe
commit 8d2f2fa11c
8 changed files with 218 additions and 26 deletions
+17 -1
View File
@@ -10,7 +10,7 @@ import time
import pytest
from thefuck import shell_ast
from thefuck import shell_ast, typo
from thefuck.resolvers.help_resolver import get_help_correction
@@ -95,6 +95,22 @@ def test_returns_correction_when_subcommand_misspelled(
assert _spawned(argv_log) == ['--help']
def test_corrects_transposed_subcommand(fake_bin, argv_log, enable_cache):
# bulid -> build scores 2 * 4 / 10 = 0.8, right at the cutoff,
# and is also a single transposition (li <-> il).
fake_bin('fake')
assert get_help_correction('fake bulid .') == 'fake build .'
assert typo.single_edit('bulid', 'build')
def test_corrects_transposition_under_cutoff(
fake_bin, argv_log, enable_cache):
# psuh -> push scores 2 * 3 / 8 = 0.75, under the cutoff, so only
# the first-char-equal single-edit match fixes it.
fake_bin('fake', commands=('push', 'build', 'clean'))
assert get_help_correction('fake psuh x') == 'fake push x'
def test_respawns_when_binary_mtime_changes(
fake_bin, argv_log, enable_cache):
path = fake_bin('fake')
+19 -11
View File
@@ -2,7 +2,7 @@ import difflib
import pytest
from thefuck import shell_ast
from thefuck import shell_ast, typo
from thefuck.resolvers.history_resolver import (
_CANDIDATES,
_TOKEN_CUTOFF,
@@ -42,20 +42,26 @@ def test_corrects_two_diverged_tokens_across_segments(history):
assert get_history_correction(command) == 'git status | grep -i foo'
def test_declines_tokens_below_cutoff(history):
# The plan's flagship pair lands under the mandated token cutoff:
# SequenceMatcher(None, 'psuh', 'push').ratio() == 0.75 and
# SequenceMatcher(None, 'greo', 'grep').ratio() == 0.75, both
# below _TOKEN_CUTOFF, so the safety gate must decline them.
@requires_ast
def test_corrects_transposed_tokens_under_ratio_cutoff(history):
# psuh -> push and greo -> grep both score 2 * 3 / 8 = 0.75,
# under _TOKEN_CUTOFF, but each is a first-char-equal single
# transposition, which the token gate accepts alongside the
# ratio.
history(['git push | grep -i foo'])
command = Command('git psuh | greo -i foo', '')
assert get_history_correction(command) is None
assert get_history_correction(command) == 'git push | grep -i foo'
def test_declines_token_just_below_cutoff(history):
# '--ignore-whitespaces' vs '--ignore-all-spaces' scores
# 2 * 15 / 39 = 0.769 (under _TOKEN_CUTOFF) AND spans two edits
# (pinned below), so even the amended single-edit gate declines.
history(['git diff --ignore-all-spaces HEAD'])
command = Command('git diff --ignore-whitespaces HEAD', '')
assert get_history_correction(command) is None
assert not typo.single_edit('--ignore-whitespaces',
'--ignore-all-spaces')
def test_token_cutoff_boundary_arithmetic():
@@ -115,13 +121,15 @@ def test_declines_when_history_is_only_unparseable(history):
assert get_history_correction(command) is None
def test_flat_mode_declines_multi_segment_scripts(history, monkeypatch):
# With the parser unavailable the pipeline cannot be verified
# structurally and both diverged tokens fall under the cutoff.
def test_flat_mode_corrects_single_edit_tokens(history, monkeypatch):
# With the parser unavailable both scripts take the flat view;
# token counts still line up and both diverged tokens are
# first-char-equal single edits, so the amended gate corrects
# them there too.
monkeypatch.setattr(shell_ast, 'AST_AVAILABLE', False)
history(['git push | grep -i foo'])
command = Command('git psuh | greo -i foo', '')
assert get_history_correction(command) is None
assert get_history_correction(command) == 'git push | grep -i foo'
def test_declines_history_below_prefilter_cutoff(history):