Skip to content

Commit

Permalink
nvbn#842: add settings var to control number of close matches
Browse files Browse the repository at this point in the history
  • Loading branch information
scorphus committed Oct 5, 2018
1 parent 087c58f commit 5ae637c
Show file tree
Hide file tree
Showing 12 changed files with 101 additions and 18 deletions.
8 changes: 6 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -390,7 +390,8 @@ Several *The Fuck* parameters can be changed in the file `$XDG_CONFIG_HOME/thefu
* `history_limit` – numeric value of how many history commands will be scanned, like `2000`;
* `alter_history` – push fixed command to history, by default `True`;
* `wait_slow_command` – max amount of time in seconds for getting previous command output if it in `slow_commands` list;
* `slow_commands` – list of slow commands.
* `slow_commands` – list of slow commands;
* `num_close_matches` – maximum number of close matches to suggest, by default `3`.

An example of `settings.py`:

Expand All @@ -405,6 +406,7 @@ debug = False
history_limit = 9999
wait_slow_command = 20
slow_commands = ['react-native', 'gradle']
num_close_matches = 5
```

Or via environment variables:
Expand All @@ -420,7 +422,8 @@ rule with lower `priority` will be matched first;
* `THEFUCK_HISTORY_LIMIT` – how many history commands will be scanned, like `2000`;
* `THEFUCK_ALTER_HISTORY` – push fixed command to history `true/false`;
* `THEFUCK_WAIT_SLOW_COMMAND` – max amount of time in seconds for getting previous command output if it in `slow_commands` list;
* `THEFUCK_SLOW_COMMANDS` – list of slow commands, like `lein:gradle`.
* `THEFUCK_SLOW_COMMANDS` – list of slow commands, like `lein:gradle`;
* `THEFUCK_NUM_CLOSE_MATCHES` – maximum number of close matches to suggest, like `5`.

For example:

Expand All @@ -432,6 +435,7 @@ export THEFUCK_WAIT_COMMAND=10
export THEFUCK_NO_COLORS='false'
export THEFUCK_PRIORITY='no_command=9999:apt_get=100'
export THEFUCK_HISTORY_LIMIT='2000'
export THEFUCK_NUM_CLOSE_MATCHES='5'
```

## Third-party packages with rules
Expand Down
58 changes: 58 additions & 0 deletions tests/output_readers/test_rerun.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# -*- encoding: utf-8 -*-

from mock import Mock, patch
from psutil import AccessDenied, TimeoutExpired

from thefuck.output_readers import rerun


class TestRerun(object):
def setup_method(self, test_method):
self.patcher = patch('thefuck.output_readers.rerun.Process')
process_mock = self.patcher.start()
self.proc_mock = process_mock.return_value = Mock()

def teardown_method(self, test_method):
self.patcher.stop()

@patch('thefuck.output_readers.rerun._wait_output', return_value=False)
@patch('thefuck.output_readers.rerun.Popen')
def test_get_output(self, popen_mock, wait_output_mock):
popen_mock.return_value.stdout.read.return_value = b'output'
assert rerun.get_output('', '') is None
wait_output_mock.assert_called_once()

def test_wait_output_is_slow(self, settings):
assert rerun._wait_output(Mock(), True)
self.proc_mock.wait.assert_called_once_with(settings.wait_slow_command)

def test_wait_output_is_not_slow(self, settings):
assert rerun._wait_output(Mock(), False)
self.proc_mock.wait.assert_called_once_with(settings.wait_command)

@patch('thefuck.output_readers.rerun._kill_process')
def test_wait_output_timeout(self, kill_process_mock):
self.proc_mock.wait.side_effect = TimeoutExpired(3)
self.proc_mock.children.return_value = []
assert not rerun._wait_output(Mock(), False)
kill_process_mock.assert_called_once_with(self.proc_mock)

@patch('thefuck.output_readers.rerun._kill_process')
def test_wait_output_timeout_children(self, kill_process_mock):
self.proc_mock.wait.side_effect = TimeoutExpired(3)
self.proc_mock.children.return_value = [Mock()] * 2
assert not rerun._wait_output(Mock(), False)
assert kill_process_mock.call_count == 3

def test_kill_process(self):
proc = Mock()
rerun._kill_process(proc)
proc.kill.assert_called_once_with()

@patch('thefuck.output_readers.rerun.logs')
def test_kill_process_access_denied(self, logs_mock):
proc = Mock()
proc.kill.side_effect = AccessDenied()
rerun._kill_process(proc)
proc.kill.assert_called_once_with()
logs_mock.debug.assert_called_once()
4 changes: 3 additions & 1 deletion tests/test_conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,8 @@ def test_from_env(self, os_environ, settings):
'THEFUCK_NO_COLORS': 'false',
'THEFUCK_PRIORITY': 'bash=10:lisp=wrong:vim=15',
'THEFUCK_WAIT_SLOW_COMMAND': '999',
'THEFUCK_SLOW_COMMANDS': 'lein:react-native:./gradlew'})
'THEFUCK_SLOW_COMMANDS': 'lein:react-native:./gradlew',
'THEFUCK_NUM_CLOSE_MATCHES': '359'})
settings.init()
assert settings.rules == ['bash', 'lisp']
assert settings.exclude_rules == ['git', 'vim']
Expand All @@ -63,6 +64,7 @@ def test_from_env(self, os_environ, settings):
assert settings.priority == {'bash': 10, 'vim': 15}
assert settings.wait_slow_command == 999
assert settings.slow_commands == ['lein', 'react-native', './gradlew']
assert settings.num_close_matches == 359

def test_from_env_with_DEFAULT(self, os_environ, settings):
os_environ.update({'THEFUCK_RULES': 'DEFAULT_RULES:bash:lisp'})
Expand Down
16 changes: 14 additions & 2 deletions tests/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@

import pytest
import warnings
from mock import Mock
from mock import Mock, patch
from thefuck.utils import default_settings, \
memoize, get_closest, get_all_executables, replace_argument, \
get_all_matched_commands, is_app, for_app, cache, \
get_valid_history_without_current, _cache
get_valid_history_without_current, _cache, get_close_matches
from thefuck.types import Command


Expand Down Expand Up @@ -50,6 +50,18 @@ def test_without_fallback(self):
fallback_to_first=False) is None


class TestGetCloseMatches(object):
@patch('thefuck.utils.difflib_get_close_matches')
def test_call_with_n(self, difflib_mock):
get_close_matches('', [], 1)
assert difflib_mock.call_args[0][2] == 1

@patch('thefuck.utils.difflib_get_close_matches')
def test_call_without_n(self, difflib_mock, settings):
get_close_matches('', [])
assert difflib_mock.call_args[0][2] == settings.get('num_close_matches')


@pytest.fixture
def get_aliases(mocker):
mocker.patch('thefuck.shells.shell.get_aliases',
Expand Down
3 changes: 2 additions & 1 deletion thefuck/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,8 @@ def _val_from_env(self, env, attr):
return self._rules_from_env(val)
elif attr == 'priority':
return dict(self._priority_from_env(val))
elif attr in ('wait_command', 'history_limit', 'wait_slow_command'):
elif attr in ('wait_command', 'history_limit', 'wait_slow_command',
'num_close_matches'):
return int(val)
elif attr in ('require_confirmation', 'no_colors', 'debug',
'alter_history', 'instant_mode'):
Expand Down
4 changes: 3 additions & 1 deletion thefuck/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ def __repr__(self):
'./gradlew', 'vagrant'],
'repeat': False,
'instant_mode': False,
'num_close_matches': 3,
'env': {'LC_ALL': 'C', 'LANG': 'C', 'GIT_TRACE': '1'}}

ENV_TO_ATTR = {'THEFUCK_RULES': 'rules',
Expand All @@ -56,7 +57,8 @@ def __repr__(self):
'THEFUCK_WAIT_SLOW_COMMAND': 'wait_slow_command',
'THEFUCK_SLOW_COMMANDS': 'slow_commands',
'THEFUCK_REPEAT': 'repeat',
'THEFUCK_INSTANT_MODE': 'instant_mode'}
'THEFUCK_INSTANT_MODE': 'instant_mode',
'THEFUCK_NUM_CLOSE_MATCHES': 'num_close_matches'}

SETTINGS_HEADER = u"""# The Fuck settings file
#
Expand Down
3 changes: 1 addition & 2 deletions thefuck/rules/cd_correction.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,9 @@

import os
import six
from difflib import get_close_matches
from thefuck.specific.sudo import sudo_support
from thefuck.rules import cd_mkdir
from thefuck.utils import for_app
from thefuck.utils import for_app, get_close_matches

__author__ = "mmussomele"

Expand Down
2 changes: 1 addition & 1 deletion thefuck/rules/git_rebase_merge_dir.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from difflib import get_close_matches
from thefuck.utils import get_close_matches
from thefuck.specific.git import git_support


Expand Down
4 changes: 2 additions & 2 deletions thefuck/rules/history.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from difflib import get_close_matches
from thefuck.utils import get_closest, get_valid_history_without_current
from thefuck.utils import get_close_matches, get_closest, \
get_valid_history_without_current


def match(command):
Expand Down
3 changes: 1 addition & 2 deletions thefuck/rules/mvn_unknown_lifecycle_phase.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
from thefuck.utils import replace_command, for_app
from difflib import get_close_matches
from thefuck.utils import for_app, get_close_matches, replace_command
import re


Expand Down
3 changes: 1 addition & 2 deletions thefuck/rules/no_command.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
from difflib import get_close_matches
from thefuck.utils import get_all_executables, \
from thefuck.utils import get_all_executables, get_close_matches, \
get_valid_history_without_current, get_closest, which
from thefuck.specific.sudo import sudo_support

Expand Down
11 changes: 9 additions & 2 deletions thefuck/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import shelve
import six
from decorator import decorator
from difflib import get_close_matches
from difflib import get_close_matches as difflib_get_close_matches
from functools import wraps
from .logs import warn
from .conf import settings
Expand Down Expand Up @@ -90,12 +90,19 @@ def get_closest(word, possibilities, n=3, cutoff=0.6, fallback_to_first=True):
"""Returns closest match or just first from possibilities."""
possibilities = list(possibilities)
try:
return get_close_matches(word, possibilities, n, cutoff)[0]
return difflib_get_close_matches(word, possibilities, n, cutoff)[0]
except IndexError:
if fallback_to_first:
return possibilities[0]


def get_close_matches(word, possibilities, n=None, cutoff=0.6):
"""Overrides `difflib.get_close_match` to controle argument `n`."""
if n is None:
n = settings.num_close_matches
return difflib_get_close_matches(word, possibilities, n, cutoff)


@memoize
def get_all_executables():
from thefuck.shells import shell
Expand Down

0 comments on commit 5ae637c

Please sign in to comment.