diff --git a/README.md b/README.md index 2dc629c6..a2977da8 100644 --- a/README.md +++ b/README.md @@ -169,6 +169,7 @@ using the matched rule and runs it. Rules enabled by default are as follows: * `go_run` – appends `.go` extension when compiling/running Go programs * `grep_recursive` – adds `-r` when you trying to grep directory; * `has_exists_script` – prepends `./` when script/binary exists; +* `history` – tries to replace command with most similar command from history; * `java` – removes `.java` extension when running Java programs; * `javac` – appends missing `.java` when compiling Java files; * `lein_not_task` – fixes wrong `lein` tasks like `lein rpl`; diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 00000000..e7f55e9c --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,6 @@ +import pytest + + +@pytest.fixture +def no_memoize(monkeypatch): + monkeypatch.setattr('thefuck.utils.memoize.disabled', True) diff --git a/tests/rules/test_history.py b/tests/rules/test_history.py new file mode 100644 index 00000000..0a6065d7 --- /dev/null +++ b/tests/rules/test_history.py @@ -0,0 +1,35 @@ +import pytest +from thefuck.rules.history import match, get_new_command +from tests.utils import Command + + +@pytest.fixture +def history(mocker): + return mocker.patch('thefuck.rules.history.get_history', + return_value=['ls cat', 'diff x', 'nocommand x']) + + +@pytest.fixture +def callables(mocker): + return mocker.patch('thefuck.rules.history.get_all_callables', + return_value=['diff', 'ls']) + + +@pytest.mark.usefixtures('history', 'callables', 'no_memoize') +@pytest.mark.parametrize('script', ['ls cet', 'daff x']) +def test_match(script): + assert match(Command(script=script), None) + + +@pytest.mark.usefixtures('history', 'callables', 'no_memoize') +@pytest.mark.parametrize('script', ['apt-get', 'nocommand y']) +def test_not_match(script): + assert not match(Command(script=script), None) + + +@pytest.mark.usefixtures('history', 'callables', 'no_memoize') +@pytest.mark.parametrize('script, result', [ + ('ls cet', 'ls cat'), + ('daff x', 'diff x')]) +def test_get_new_command(script, result): + assert get_new_command(Command(script), None) == result diff --git a/tests/rules/test_no_command.py b/tests/rules/test_no_command.py index 167deca1..a93d8d56 100644 --- a/tests/rules/test_no_command.py +++ b/tests/rules/test_no_command.py @@ -1,36 +1,42 @@ -from mock import patch, Mock -from thefuck.rules.no_command import match, get_new_command, _get_all_callables +import pytest +from thefuck.rules.no_command import match, get_new_command, get_all_callables +from tests.utils import Command -@patch('thefuck.rules.no_command._safe', return_value=[]) -@patch('thefuck.rules.no_command.get_aliases', - return_value=['vim', 'apt-get', 'fsck', 'fuck']) +@pytest.fixture(autouse=True) +def _safe(mocker): + mocker.patch('thefuck.rules.no_command._safe', return_value=[]) + + +@pytest.fixture(autouse=True) +def get_aliases(mocker): + mocker.patch('thefuck.rules.no_command.get_aliases', + return_value=['vim', 'apt-get', 'fsck', 'fuck']) + + +@pytest.mark.usefixtures('no_memoize') def test_get_all_callables(*args): - all_callables = _get_all_callables() + all_callables = get_all_callables() assert 'vim' in all_callables assert 'fsck' in all_callables assert 'fuck' not in all_callables -@patch('thefuck.rules.no_command._safe', return_value=[]) -@patch('thefuck.rules.no_command.get_aliases', - return_value=['vim', 'apt-get', 'fsck', 'fuck']) +@pytest.mark.usefixtures('no_memoize') def test_match(*args): - assert match(Mock(stderr='vom: not found', script='vom file.py'), None) - assert match(Mock(stderr='fucck: not found', script='fucck'), None) - assert not match(Mock(stderr='qweqwe: not found', script='qweqwe'), None) - assert not match(Mock(stderr='some text', script='vom file.py'), None) + assert match(Command(stderr='vom: not found', script='vom file.py'), None) + assert match(Command(stderr='fucck: not found', script='fucck'), None) + assert not match(Command(stderr='qweqwe: not found', script='qweqwe'), None) + assert not match(Command(stderr='some text', script='vom file.py'), None) -@patch('thefuck.rules.no_command._safe', return_value=[]) -@patch('thefuck.rules.no_command.get_aliases', - return_value=['vim', 'apt-get', 'fsck', 'fuck']) +@pytest.mark.usefixtures('no_memoize') def test_get_new_command(*args): assert get_new_command( - Mock(stderr='vom: not found', - script='vom file.py'), + Command(stderr='vom: not found', + script='vom file.py'), None) == 'vim file.py' assert get_new_command( - Mock(stderr='fucck: not found', - script='fucck'), - None) == 'fsck' + Command(stderr='fucck: not found', + script='fucck'), + Command) == 'fsck' diff --git a/tests/test_utils.py b/tests/test_utils.py index 56d29ec8..0352fd7b 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -2,7 +2,7 @@ import pytest from mock import Mock from thefuck.utils import sudo_support, wrap_settings, memoize, get_closest from thefuck.types import Settings -from tests.utils import Command, no_memoize +from tests.utils import Command @pytest.mark.parametrize('override, old, new', [ diff --git a/tests/utils.py b/tests/utils.py index 0f0f0fd5..4641971d 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -1,4 +1,3 @@ -import pytest from thefuck import types from thefuck.conf import DEFAULT_PRIORITY @@ -15,7 +14,3 @@ def Rule(name='', match=lambda *_: True, return types.Rule(name, match, get_new_command, enabled_by_default, side_effect, priority) - -@pytest.fixture -def no_memoize(monkeypatch): - monkeypatch.setattr('thefuck.utils.memoize.disabled', True) diff --git a/thefuck/rules/history.py b/thefuck/rules/history.py new file mode 100644 index 00000000..257c3a8e --- /dev/null +++ b/thefuck/rules/history.py @@ -0,0 +1,24 @@ +from difflib import get_close_matches +from thefuck.shells import get_history +from thefuck.utils import get_closest, memoize +from thefuck.rules.no_command import get_all_callables + + +@memoize +def _history_of_exists_without_current(command): + callables = get_all_callables() + return [line for line in get_history() + if line != command.script + and line.split(' ')[0] in callables] + + +def match(command, settings): + return len(get_close_matches(command.script, + _history_of_exists_without_current(command))) + + +def get_new_command(command, settings): + return get_closest(command.script, + _history_of_exists_without_current(command)) + +priority = 9999 diff --git a/thefuck/rules/no_command.py b/thefuck/rules/no_command.py index 2f6337b8..f6d9f52b 100644 --- a/thefuck/rules/no_command.py +++ b/thefuck/rules/no_command.py @@ -1,7 +1,7 @@ from difflib import get_close_matches import os from pathlib import Path -from thefuck.utils import sudo_support +from thefuck.utils import sudo_support, memoize from thefuck.shells import thefuck_alias, get_aliases @@ -12,7 +12,8 @@ def _safe(fn, fallback): return fallback -def _get_all_callables(): +@memoize +def get_all_callables(): tf_alias = thefuck_alias() return [exe.name for path in os.environ.get('PATH', '').split(':') @@ -25,14 +26,14 @@ def _get_all_callables(): def match(command, settings): return 'not found' in command.stderr and \ bool(get_close_matches(command.script.split(' ')[0], - _get_all_callables())) + get_all_callables())) @sudo_support def get_new_command(command, settings): old_command = command.script.split(' ')[0] new_command = get_close_matches(old_command, - _get_all_callables())[0] + get_all_callables())[0] return ' '.join([new_command] + command.script.split(' ')[1:])