From 0d86fce9befd5bd1d73d7cb780fdddf7af3058a6 Mon Sep 17 00:00:00 2001 From: Michael Lee Date: Wed, 26 Aug 2015 14:01:36 +0100 Subject: [PATCH 01/10] #35 mvn will auto add clean package --- README.md | 1 + tests/rules/test_mvn_no_command.py | 40 ++++++++++++++++++++++++++++++ thefuck/rules/mvn_no_command.py | 8 ++++++ 3 files changed, 49 insertions(+) create mode 100644 tests/rules/test_mvn_no_command.py create mode 100644 thefuck/rules/mvn_no_command.py diff --git a/README.md b/README.md index 97354dec..a78cd643 100644 --- a/README.md +++ b/README.md @@ -181,6 +181,7 @@ using the matched rule and runs it. Rules enabled by default are as follows: * `man_no_space` – fixes man commands without spaces, for example `mandiff`; * `mercurial` – fixes wrong `hg` commands; * `mkdir_p` – adds `-p` when you trying to create directory without parent; +* `mvn_no_command` – adds `clean package` to `mvn`; * `no_command` – fixes wrong console commands, for example `vom/vim`; * `no_such_file` – creates missing directories with `mv` and `cp` commands; * `open` – prepends `http` to address passed to `open`; diff --git a/tests/rules/test_mvn_no_command.py b/tests/rules/test_mvn_no_command.py new file mode 100644 index 00000000..5725adde --- /dev/null +++ b/tests/rules/test_mvn_no_command.py @@ -0,0 +1,40 @@ +import pytest +from thefuck.rules.mvn_no_command import match, get_new_command +from tests.utils import Command + + +@pytest.mark.parametrize('command', [ + Command(script='mvn', stdout='[ERROR] No goals have been specified for this build. You must specify a valid lifecycle phase or a goal in the format : or :[:]:. Available lifecycle phases are: validate, initialize, generate-sources, process-sources, generate-resources, process-resources, compile, process-classes, generate-test-sources, process-test-sources, generate-test-resources, process-test-resources, test-compile, process-test-classes, test, prepare-package, package, pre-integration-test, integration-test, post-integration-test, verify, install, deploy, pre-clean, clean, post-clean, pre-site, site, post-site, site-deploy. -> [Help 1]')]) +def test_match(command): + assert match(command, None) + + +@pytest.mark.parametrize('command', [ + Command(script='mvn clean', stdout=""" +[INFO] Scanning for projects...[INFO] +[INFO] ------------------------------------------------------------------------ +[INFO] Building test 0.2 +[INFO] ------------------------------------------------------------------------ +[INFO] +[INFO] --- maven-clean-plugin:2.5:clean (default-clean) @ test --- +[INFO] Deleting /home/mlk/code/test/target +[INFO] ------------------------------------------------------------------------ +[INFO] BUILD SUCCESS +[INFO] ------------------------------------------------------------------------ +[INFO] Total time: 0.477s +[INFO] Finished at: Wed Aug 26 13:05:47 BST 2015 +[INFO] Final Memory: 6M/240M +[INFO] ------------------------------------------------------------------------ +"""), + Command(script='mvn --help'), + Command(script='mvn -v') +]) +def test_not_match(command): + assert not match(command, None) + +@pytest.mark.parametrize('command, new_command', [ + (Command(script='mvn', stdout='[ERROR] No goals have been specified for this build. You must specify a valid lifecycle phase or a goal in the format : or :[:]:. Available lifecycle phases are: validate, initialize, generate-sources, process-sources, generate-resources, process-resources, compile, process-classes, generate-test-sources, process-test-sources, generate-test-resources, process-test-resources, test-compile, process-test-classes, test, prepare-package, package, pre-integration-test, integration-test, post-integration-test, verify, install, deploy, pre-clean, clean, post-clean, pre-site, site, post-site, site-deploy. -> [Help 1]'), ['mvn clean package', 'mvn clean install']), + (Command(script='mvn -N', stdout='[ERROR] No goals have been specified for this build. You must specify a valid lifecycle phase or a goal in the format : or :[:]:. Available lifecycle phases are: validate, initialize, generate-sources, process-sources, generate-resources, process-resources, compile, process-classes, generate-test-sources, process-test-sources, generate-test-resources, process-test-resources, test-compile, process-test-classes, test, prepare-package, package, pre-integration-test, integration-test, post-integration-test, verify, install, deploy, pre-clean, clean, post-clean, pre-site, site, post-site, site-deploy. -> [Help 1]'), ['mvn -N clean package', 'mvn -N clean install'])]) +def test_get_new_command(command, new_command): + assert get_new_command(command, None) == new_command + diff --git a/thefuck/rules/mvn_no_command.py b/thefuck/rules/mvn_no_command.py new file mode 100644 index 00000000..53c71060 --- /dev/null +++ b/thefuck/rules/mvn_no_command.py @@ -0,0 +1,8 @@ +from thefuck import shells + +def match(command, settings): + return 'No goals have been specified for this build' in command.stdout and command.script.startswith('mvn') + + +def get_new_command(command, settings): + return [ command.script + ' clean package', command.script + ' clean install'] From 301de75aeed2f502a405ad9b81e005e5b46dcfa4 Mon Sep 17 00:00:00 2001 From: Michael Lee Date: Wed, 26 Aug 2015 14:58:45 +0100 Subject: [PATCH 02/10] #35 - Fuzzy matching on maven lifecycle targets --- README.md | 1 + .../rules/test_mvn_unknown_lifecycle_phase.py | 41 +++++++++++++++++++ thefuck/rules/mvn_unknown_lifecycle_phase.py | 20 +++++++++ 3 files changed, 62 insertions(+) create mode 100644 tests/rules/test_mvn_unknown_lifecycle_phase.py create mode 100644 thefuck/rules/mvn_unknown_lifecycle_phase.py diff --git a/README.md b/README.md index a78cd643..93832b9f 100644 --- a/README.md +++ b/README.md @@ -182,6 +182,7 @@ using the matched rule and runs it. Rules enabled by default are as follows: * `mercurial` – fixes wrong `hg` commands; * `mkdir_p` – adds `-p` when you trying to create directory without parent; * `mvn_no_command` – adds `clean package` to `mvn`; +* `mvn_unknown_lifecycle_phase` – fixes miss spelt lifecycle phases with `mvn`; * `no_command` – fixes wrong console commands, for example `vom/vim`; * `no_such_file` – creates missing directories with `mv` and `cp` commands; * `open` – prepends `http` to address passed to `open`; diff --git a/tests/rules/test_mvn_unknown_lifecycle_phase.py b/tests/rules/test_mvn_unknown_lifecycle_phase.py new file mode 100644 index 00000000..47ff4470 --- /dev/null +++ b/tests/rules/test_mvn_unknown_lifecycle_phase.py @@ -0,0 +1,41 @@ +import pytest +from thefuck.rules.mvn_unknown_lifecycle_phase import match, get_new_command +from tests.utils import Command + + +@pytest.mark.parametrize('command', [ + Command(script='mvn cle', stdout='[ERROR] Unknown lifecycle phase "cle". You must specify a valid lifecycle phase or a goal in the format : or :[:]:. Available lifecycle phases are: validate, initialize, generate-sources, process-sources, generate-resources, process-resources, compile, process-classes, generate-test-sources, process-test-sources, generate-test-resources, process-test-resources, test-compile, process-test-classes, test, prepare-package, package, pre-integration-test, integration-test, post-integration-test, verify, install, deploy, pre-clean, clean, post-clean, pre-site, site, post-site, site-deploy. -> [Help 1]')]) +def test_match(command): + assert match(command, None) + + +@pytest.mark.parametrize('command', [ + Command(script='mvn clean', stdout=""" +[INFO] Scanning for projects...[INFO] +[INFO] ------------------------------------------------------------------------ +[INFO] Building test 0.2 +[INFO] ------------------------------------------------------------------------ +[INFO] +[INFO] --- maven-clean-plugin:2.5:clean (default-clean) @ test --- +[INFO] Deleting /home/mlk/code/test/target +[INFO] ------------------------------------------------------------------------ +[INFO] BUILD SUCCESS +[INFO] ------------------------------------------------------------------------ +[INFO] Total time: 0.477s +[INFO] Finished at: Wed Aug 26 13:05:47 BST 2015 +[INFO] Final Memory: 6M/240M +[INFO] ------------------------------------------------------------------------ +"""), + Command(script='mvn --help'), + Command(script='mvn -v') +]) +def test_not_match(command): + assert not match(command, None) + +@pytest.mark.parametrize('command, new_command', [ + (Command(script='mvn cle', stdout='[ERROR] Unknown lifecycle phase "cle". You must specify a valid lifecycle phase or a goal in the format : or :[:]:. Available lifecycle phases are: validate, initialize, generate-sources, process-sources, generate-resources, process-resources, compile, process-classes, generate-test-sources, process-test-sources, generate-test-resources, process-test-resources, test-compile, process-test-classes, test, prepare-package, package, pre-integration-test, integration-test, post-integration-test, verify, install, deploy, pre-clean, clean, post-clean, pre-site, site, post-site, site-deploy. -> [Help 1]'), ['mvn clean', 'mvn compile']), + (Command(script='mvn claen package', stdout='[ERROR] Unknown lifecycle phase "claen". You must specify a valid lifecycle phase or a goal in the format : or :[:]:. Available lifecycle phases are: validate, initialize, generate-sources, process-sources, generate-resources, process-resources, compile, process-classes, generate-test-sources, process-test-sources, generate-test-resources, process-test-resources, test-compile, process-test-classes, test, prepare-package, package, pre-integration-test, integration-test, post-integration-test, verify, install, deploy, pre-clean, clean, post-clean, pre-site, site, post-site, site-deploy. -> [Help 1]'), ['mvn clean package'])]) +def test_get_new_command(command, new_command): + print new_command + assert get_new_command(command, None) == new_command + diff --git a/thefuck/rules/mvn_unknown_lifecycle_phase.py b/thefuck/rules/mvn_unknown_lifecycle_phase.py new file mode 100644 index 00000000..1dcf1ad1 --- /dev/null +++ b/thefuck/rules/mvn_unknown_lifecycle_phase.py @@ -0,0 +1,20 @@ +from thefuck import shells +from thefuck.utils import replace_command +from difflib import get_close_matches +import re + + +def match(command, settings): + failedLifecycle = re.search('\[ERROR\] Unknown lifecycle phase "(.+)"', command.stdout) + availableLifecycles = re.search('Available lifecycle phases are: (.+) -> \[Help 1\]', command.stdout) + return availableLifecycles and failedLifecycle and command.script.startswith('mvn') + + +def get_new_command(command, settings): + failedLifecycle = re.search('\[ERROR\] Unknown lifecycle phase "(.+)"', command.stdout) + availableLifecycles = re.search('Available lifecycle phases are: (.+) -> \[Help 1\]', command.stdout) + if availableLifecycles and failedLifecycle: + selectedLifecycle = get_close_matches(failedLifecycle.group(1), availableLifecycles.group(1).split(", "), 3, 0.6) + return replace_command(command, failedLifecycle.group(1), selectedLifecycle) + else: + return [] From 8c02658a3278c02fc890792f59ba8ecace90d8b9 Mon Sep 17 00:00:00 2001 From: Michael Lee Date: Wed, 26 Aug 2015 15:02:46 +0100 Subject: [PATCH 03/10] Removed debug statement --- tests/rules/test_mvn_unknown_lifecycle_phase.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/rules/test_mvn_unknown_lifecycle_phase.py b/tests/rules/test_mvn_unknown_lifecycle_phase.py index 47ff4470..421325d1 100644 --- a/tests/rules/test_mvn_unknown_lifecycle_phase.py +++ b/tests/rules/test_mvn_unknown_lifecycle_phase.py @@ -36,6 +36,5 @@ def test_not_match(command): (Command(script='mvn cle', stdout='[ERROR] Unknown lifecycle phase "cle". You must specify a valid lifecycle phase or a goal in the format : or :[:]:. Available lifecycle phases are: validate, initialize, generate-sources, process-sources, generate-resources, process-resources, compile, process-classes, generate-test-sources, process-test-sources, generate-test-resources, process-test-resources, test-compile, process-test-classes, test, prepare-package, package, pre-integration-test, integration-test, post-integration-test, verify, install, deploy, pre-clean, clean, post-clean, pre-site, site, post-site, site-deploy. -> [Help 1]'), ['mvn clean', 'mvn compile']), (Command(script='mvn claen package', stdout='[ERROR] Unknown lifecycle phase "claen". You must specify a valid lifecycle phase or a goal in the format : or :[:]:. Available lifecycle phases are: validate, initialize, generate-sources, process-sources, generate-resources, process-resources, compile, process-classes, generate-test-sources, process-test-sources, generate-test-resources, process-test-resources, test-compile, process-test-classes, test, prepare-package, package, pre-integration-test, integration-test, post-integration-test, verify, install, deploy, pre-clean, clean, post-clean, pre-site, site, post-site, site-deploy. -> [Help 1]'), ['mvn clean package'])]) def test_get_new_command(command, new_command): - print new_command assert get_new_command(command, None) == new_command From 9103c1ffd52315df62b2401cc07c702fae3b3eab Mon Sep 17 00:00:00 2001 From: nvbn Date: Thu, 27 Aug 2015 16:08:29 +0300 Subject: [PATCH 04/10] Add is_app/for_app helpers --- tests/test_utils.py | 25 ++++++++++++++++++++++++- thefuck/utils.py | 23 +++++++++++++++++++++++ 2 files changed, 47 insertions(+), 1 deletion(-) diff --git a/tests/test_utils.py b/tests/test_utils.py index b66f4c8e..5979b204 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -2,8 +2,9 @@ import pytest from mock import Mock from thefuck.utils import wrap_settings,\ memoize, get_closest, get_all_executables, replace_argument, \ - get_all_matched_commands + get_all_matched_commands, is_app, for_app from thefuck.types import Settings +from tests.utils import Command @pytest.mark.parametrize('override, old, new', [ @@ -93,3 +94,25 @@ def test_replace_argument(args, result): 'service-status', 'service-unbind'])]) def test_get_all_matched_commands(stderr, result): assert list(get_all_matched_commands(stderr)) == result + + +@pytest.mark.usefixtures('no_memoize') +@pytest.mark.parametrize('script, names, result', [ + ('git diff', ['git', 'hub'], True), + ('hub diff', ['git', 'hub'], True), + ('hg diff', ['git', 'hub'], False)]) +def test_is_app(script, names, result): + assert is_app(Command(script), *names) == result + + +@pytest.mark.usefixtures('no_memoize') +@pytest.mark.parametrize('script, names, result', [ + ('git diff', ['git', 'hub'], True), + ('hub diff', ['git', 'hub'], True), + ('hg diff', ['git', 'hub'], False)]) +def test_for_app(script, names, result): + @for_app(*names) + def match(command, settings): + return True + + assert match(Command(script), None) == result diff --git a/thefuck/utils.py b/thefuck/utils.py index 8dd777b7..6091e7f0 100644 --- a/thefuck/utils.py +++ b/thefuck/utils.py @@ -132,3 +132,26 @@ def replace_command(command, broken, matched): new_cmds = get_close_matches(broken, matched, cutoff=0.1) return [replace_argument(command.script, broken, new_cmd.strip()) for new_cmd in new_cmds] + + +@memoize +def is_app(command, *app_names): + """Returns `True` if command is call to one of passed app names.""" + for name in app_names: + if command.script.startswith(u'{} '.format(name)): + return True + return False + + +def for_app(*app_names): + """Specifies that matching script is for on of app names.""" + def decorator(fn): + @wraps(fn) + def wrapper(command, settings): + if is_app(command, *app_names): + return fn(command, settings) + else: + return False + + return wrapper + return decorator From f2a7364e8caa8cd361fb59211aba4e928f73a3ff Mon Sep 17 00:00:00 2001 From: nvbn Date: Thu, 27 Aug 2015 16:10:50 +0300 Subject: [PATCH 05/10] #351 Speed-up mvn rules, pep8 fixes --- thefuck/rules/mvn_no_command.py | 9 ++++-- thefuck/rules/mvn_unknown_lifecycle_phase.py | 32 ++++++++++++++------ thefuck/utils.py | 3 +- 3 files changed, 30 insertions(+), 14 deletions(-) diff --git a/thefuck/rules/mvn_no_command.py b/thefuck/rules/mvn_no_command.py index 53c71060..7113c574 100644 --- a/thefuck/rules/mvn_no_command.py +++ b/thefuck/rules/mvn_no_command.py @@ -1,8 +1,11 @@ -from thefuck import shells +from thefuck.utils import for_app + +@for_app('mvn') def match(command, settings): - return 'No goals have been specified for this build' in command.stdout and command.script.startswith('mvn') + return 'No goals have been specified for this build' in command.stdout def get_new_command(command, settings): - return [ command.script + ' clean package', command.script + ' clean install'] + return [command.script + ' clean package', + command.script + ' clean install'] diff --git a/thefuck/rules/mvn_unknown_lifecycle_phase.py b/thefuck/rules/mvn_unknown_lifecycle_phase.py index 1dcf1ad1..c4a7ee17 100644 --- a/thefuck/rules/mvn_unknown_lifecycle_phase.py +++ b/thefuck/rules/mvn_unknown_lifecycle_phase.py @@ -1,20 +1,32 @@ -from thefuck import shells -from thefuck.utils import replace_command +from thefuck.utils import replace_command, for_app from difflib import get_close_matches import re +def _get_failed_lifecycle(command): + return re.search(r'\[ERROR\] Unknown lifecycle phase "(.+)"', + command.stdout) + + +def _getavailable_lifecycles(command): + return re.search( + r'Available lifecycle phases are: (.+) -> \[Help 1\]', command.stdout) + + +@for_app('mvn') def match(command, settings): - failedLifecycle = re.search('\[ERROR\] Unknown lifecycle phase "(.+)"', command.stdout) - availableLifecycles = re.search('Available lifecycle phases are: (.+) -> \[Help 1\]', command.stdout) - return availableLifecycles and failedLifecycle and command.script.startswith('mvn') + failed_lifecycle = _get_failed_lifecycle(command) + available_lifecycles = _getavailable_lifecycles(command) + return available_lifecycles and failed_lifecycle def get_new_command(command, settings): - failedLifecycle = re.search('\[ERROR\] Unknown lifecycle phase "(.+)"', command.stdout) - availableLifecycles = re.search('Available lifecycle phases are: (.+) -> \[Help 1\]', command.stdout) - if availableLifecycles and failedLifecycle: - selectedLifecycle = get_close_matches(failedLifecycle.group(1), availableLifecycles.group(1).split(", "), 3, 0.6) - return replace_command(command, failedLifecycle.group(1), selectedLifecycle) + failed_lifecycle = _get_failed_lifecycle(command) + available_lifecycles = _getavailable_lifecycles(command) + if available_lifecycles and failed_lifecycle: + selected_lifecycle = get_close_matches( + failed_lifecycle.group(1), available_lifecycles.group(1).split(", "), + 3, 0.6) + return replace_command(command, failed_lifecycle.group(1), selected_lifecycle) else: return [] diff --git a/thefuck/utils.py b/thefuck/utils.py index 6091e7f0..2887397e 100644 --- a/thefuck/utils.py +++ b/thefuck/utils.py @@ -138,7 +138,8 @@ def replace_command(command, broken, matched): def is_app(command, *app_names): """Returns `True` if command is call to one of passed app names.""" for name in app_names: - if command.script.startswith(u'{} '.format(name)): + if command.script == name \ + or command.script.startswith(u'{} '.format(name)): return True return False From 0c283ff2b8fe1bea14d5b1e268226fc0f6e2a4d6 Mon Sep 17 00:00:00 2001 From: nvbn Date: Thu, 27 Aug 2015 16:42:09 +0300 Subject: [PATCH 06/10] #334 Speed-up rules with caching `for_app` decorator --- tests/rules/test_lein_not_task.py | 8 ++++---- tests/rules/test_ls_lah.py | 18 +++++++++--------- thefuck/rules/apt_get_search.py | 2 ++ thefuck/rules/cargo_no_command.py | 6 +++--- thefuck/rules/cd_correction.py | 2 ++ thefuck/rules/cd_mkdir.py | 7 ++++--- thefuck/rules/composer_not_command.py | 8 ++++---- thefuck/rules/cp_omitting_directory.py | 5 +++-- thefuck/rules/cpp11.py | 11 +++++++---- thefuck/rules/dirty_untar.py | 11 ++++++----- thefuck/rules/dirty_unzip.py | 5 +++-- thefuck/rules/docker_not_command.py | 6 +++--- thefuck/rules/git_push_force.py | 1 - thefuck/rules/go_run.py | 2 ++ thefuck/rules/grep_recursive.py | 7 +++++-- thefuck/rules/gulp_not_task.py | 6 +++--- thefuck/rules/heroku_not_command.py | 6 +++--- thefuck/rules/java.py | 17 ++++++++++------- thefuck/rules/javac.py | 19 +++++++++++-------- thefuck/rules/lein_not_task.py | 3 ++- thefuck/rules/ls_lah.py | 8 +++++--- thefuck/rules/mercurial.py | 14 ++++++-------- thefuck/rules/open.py | 24 ++++++++++++------------ thefuck/rules/pip_unknown_command.py | 5 ++++- thefuck/rules/python_execute.py | 5 +++-- thefuck/rules/sed_unterminated_s.py | 6 +++--- thefuck/rules/ssh_known_hosts.py | 2 ++ thefuck/rules/systemctl.py | 5 +++-- thefuck/rules/tmux.py | 6 +++--- thefuck/rules/tsuru_login.py | 5 +++-- thefuck/rules/tsuru_not_command.py | 6 +++--- thefuck/rules/vagrant_up.py | 4 +++- thefuck/specific/git.py | 15 ++++++--------- 33 files changed, 142 insertions(+), 113 deletions(-) diff --git a/tests/rules/test_lein_not_task.py b/tests/rules/test_lein_not_task.py index 9eef9b44..9069fcd0 100644 --- a/tests/rules/test_lein_not_task.py +++ b/tests/rules/test_lein_not_task.py @@ -1,6 +1,6 @@ import pytest -from mock import Mock from thefuck.rules.lein_not_task import match, get_new_command +from tests.utils import Command @pytest.fixture @@ -14,10 +14,10 @@ Did you mean this? def test_match(is_not_task): - assert match(Mock(script='lein rpl', stderr=is_not_task), None) - assert not match(Mock(script='ls', stderr=is_not_task), None) + assert match(Command(script='lein rpl', stderr=is_not_task), None) + assert not match(Command(script='ls', stderr=is_not_task), None) def test_get_new_command(is_not_task): - assert get_new_command(Mock(script='lein rpl --help', stderr=is_not_task), + assert get_new_command(Command(script='lein rpl --help', stderr=is_not_task), None) == ['lein repl --help', 'lein jar --help'] diff --git a/tests/rules/test_ls_lah.py b/tests/rules/test_ls_lah.py index 66bc8365..97325258 100644 --- a/tests/rules/test_ls_lah.py +++ b/tests/rules/test_ls_lah.py @@ -1,16 +1,16 @@ -from mock import patch, Mock from thefuck.rules.ls_lah import match, get_new_command +from tests.utils import Command def test_match(): - assert match(Mock(script='ls'), None) - assert match(Mock(script='ls file.py'), None) - assert match(Mock(script='ls /opt'), None) - assert not match(Mock(script='ls -lah /opt'), None) - assert not match(Mock(script='pacman -S binutils'), None) - assert not match(Mock(script='lsof'), None) + assert match(Command(script='ls'), None) + assert match(Command(script='ls file.py'), None) + assert match(Command(script='ls /opt'), None) + assert not match(Command(script='ls -lah /opt'), None) + assert not match(Command(script='pacman -S binutils'), None) + assert not match(Command(script='lsof'), None) def test_get_new_command(): - assert get_new_command(Mock(script='ls file.py'), None) == 'ls -lah file.py' - assert get_new_command(Mock(script='ls'), None) == 'ls -lah' + assert get_new_command(Command(script='ls file.py'), None) == 'ls -lah file.py' + assert get_new_command(Command(script='ls'), None) == 'ls -lah' diff --git a/thefuck/rules/apt_get_search.py b/thefuck/rules/apt_get_search.py index 6c06ddde..4454e85f 100644 --- a/thefuck/rules/apt_get_search.py +++ b/thefuck/rules/apt_get_search.py @@ -1,6 +1,8 @@ import re +from thefuck.utils import for_app +@for_app('apt-get') def match(command, settings): return command.script.startswith('apt-get search') diff --git a/thefuck/rules/cargo_no_command.py b/thefuck/rules/cargo_no_command.py index c4f6c072..ce77fe6e 100644 --- a/thefuck/rules/cargo_no_command.py +++ b/thefuck/rules/cargo_no_command.py @@ -1,10 +1,10 @@ import re -from thefuck.utils import replace_argument +from thefuck.utils import replace_argument, for_app +@for_app('cargo') def match(command, settings): - return ('cargo' in command.script - and 'No such subcommand' in command.stderr + return ('No such subcommand' in command.stderr and 'Did you mean' in command.stderr) diff --git a/thefuck/rules/cd_correction.py b/thefuck/rules/cd_correction.py index 1567ff09..33c4fc30 100644 --- a/thefuck/rules/cd_correction.py +++ b/thefuck/rules/cd_correction.py @@ -4,6 +4,7 @@ import os 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 __author__ = "mmussomele" @@ -16,6 +17,7 @@ def _get_sub_dirs(parent): @sudo_support +@for_app('cd') def match(command, settings): """Match function copied from cd_mkdir.py""" return (command.script.startswith('cd ') diff --git a/thefuck/rules/cd_mkdir.py b/thefuck/rules/cd_mkdir.py index c9ad1f7e..2262af74 100644 --- a/thefuck/rules/cd_mkdir.py +++ b/thefuck/rules/cd_mkdir.py @@ -1,13 +1,14 @@ import re from thefuck import shells +from thefuck.utils import for_app from thefuck.specific.sudo import sudo_support @sudo_support +@for_app('cd') def match(command, settings): - return (command.script.startswith('cd ') - and ('no such file or directory' in command.stderr.lower() - or 'cd: can\'t cd to' in command.stderr.lower())) + return (('no such file or directory' in command.stderr.lower() + or 'cd: can\'t cd to' in command.stderr.lower())) @sudo_support diff --git a/thefuck/rules/composer_not_command.py b/thefuck/rules/composer_not_command.py index 115ca180..abea7b34 100644 --- a/thefuck/rules/composer_not_command.py +++ b/thefuck/rules/composer_not_command.py @@ -1,11 +1,11 @@ import re -from thefuck.utils import replace_argument +from thefuck.utils import replace_argument, for_app +@for_app('composer') def match(command, settings): - return ('composer' in command.script - and ('did you mean this?' in command.stderr.lower() - or 'did you mean one of these?' in command.stderr.lower())) + return (('did you mean this?' in command.stderr.lower() + or 'did you mean one of these?' in command.stderr.lower())) def get_new_command(command, settings): diff --git a/thefuck/rules/cp_omitting_directory.py b/thefuck/rules/cp_omitting_directory.py index 9a110e0c..51fa1294 100644 --- a/thefuck/rules/cp_omitting_directory.py +++ b/thefuck/rules/cp_omitting_directory.py @@ -1,12 +1,13 @@ import re from thefuck.specific.sudo import sudo_support +from thefuck.utils import for_app @sudo_support +@for_app('cp') def match(command, settings): stderr = command.stderr.lower() - return command.script.startswith('cp ') \ - and ('omitting directory' in stderr or 'is a directory' in stderr) + return 'omitting directory' in stderr or 'is a directory' in stderr @sudo_support diff --git a/thefuck/rules/cpp11.py b/thefuck/rules/cpp11.py index 154ababc..200bf4d9 100644 --- a/thefuck/rules/cpp11.py +++ b/thefuck/rules/cpp11.py @@ -1,8 +1,11 @@ +from thefuck.utils import for_app + + +@for_app(['g++', 'clang++']) def match(command, settings): - return (('g++' in command.script or 'clang++' in command.script) and - ('This file requires compiler and library support for the ' - 'ISO C++ 2011 standard.' in command.stderr or - '-Wc++11-extensions' in command.stderr)) + return ('This file requires compiler and library support for the ' + 'ISO C++ 2011 standard.' in command.stderr or + '-Wc++11-extensions' in command.stderr) def get_new_command(command, settings): diff --git a/thefuck/rules/dirty_untar.py b/thefuck/rules/dirty_untar.py index 25300e7b..4fdf4cf6 100644 --- a/thefuck/rules/dirty_untar.py +++ b/thefuck/rules/dirty_untar.py @@ -1,6 +1,7 @@ -from thefuck import shells import os import tarfile +from thefuck import shells +from thefuck.utils import for_app def _is_tar_extract(cmd): @@ -20,19 +21,19 @@ def _tar_file(cmd): for c in cmd.split(): for ext in tar_extensions: if c.endswith(ext): - return (c, c[0:len(c)-len(ext)]) + return (c, c[0:len(c) - len(ext)]) +@for_app('tar') def match(command, settings): - return (command.script.startswith('tar') - and '-C' not in command.script + return ('-C' not in command.script and _is_tar_extract(command.script) and _tar_file(command.script) is not None) def get_new_command(command, settings): return shells.and_('mkdir -p {dir}', '{cmd} -C {dir}') \ - .format(dir=_tar_file(command.script)[1], cmd=command.script) + .format(dir=_tar_file(command.script)[1], cmd=command.script) def side_effect(old_cmd, command, settings): diff --git a/thefuck/rules/dirty_unzip.py b/thefuck/rules/dirty_unzip.py index 738cf82f..bd2d5945 100644 --- a/thefuck/rules/dirty_unzip.py +++ b/thefuck/rules/dirty_unzip.py @@ -1,5 +1,6 @@ import os import zipfile +from thefuck.utils import for_app def _is_bad_zip(file): @@ -20,9 +21,9 @@ def _zip_file(command): return '{}.zip'.format(c) +@for_app('unzip') def match(command, settings): - return (command.script.startswith('unzip') - and '-d' not in command.script + return ('-d' not in command.script and _is_bad_zip(_zip_file(command))) diff --git a/thefuck/rules/docker_not_command.py b/thefuck/rules/docker_not_command.py index 73cb8611..44578e39 100644 --- a/thefuck/rules/docker_not_command.py +++ b/thefuck/rules/docker_not_command.py @@ -1,14 +1,14 @@ from itertools import dropwhile, takewhile, islice import re import subprocess -from thefuck.utils import replace_command +from thefuck.utils import replace_command, for_app from thefuck.specific.sudo import sudo_support @sudo_support +@for_app('docker') def match(command, settings): - return command.script.startswith('docker') \ - and 'is not a docker command' in command.stderr + return 'is not a docker command' in command.stderr def get_docker_commands(): diff --git a/thefuck/rules/git_push_force.py b/thefuck/rules/git_push_force.py index 52ecfe32..45a3085a 100644 --- a/thefuck/rules/git_push_force.py +++ b/thefuck/rules/git_push_force.py @@ -1,4 +1,3 @@ -from thefuck import utils from thefuck.utils import replace_argument from thefuck.specific.git import git_support diff --git a/thefuck/rules/go_run.py b/thefuck/rules/go_run.py index b32c646a..b009324b 100644 --- a/thefuck/rules/go_run.py +++ b/thefuck/rules/go_run.py @@ -1,3 +1,4 @@ +from thefuck.utils import for_app # Appends .go when compiling go files # # Example: @@ -5,6 +6,7 @@ # error: go run: no go files listed +@for_app('go') def match(command, settings): return (command.script.startswith('go run ') and not command.script.endswith('.go')) diff --git a/thefuck/rules/grep_recursive.py b/thefuck/rules/grep_recursive.py index f2876fe1..94547b26 100644 --- a/thefuck/rules/grep_recursive.py +++ b/thefuck/rules/grep_recursive.py @@ -1,6 +1,9 @@ +from thefuck.utils import for_app + + +@for_app('grep') def match(command, settings): - return (command.script.startswith('grep') - and 'is a directory' in command.stderr.lower()) + return 'is a directory' in command.stderr.lower() def get_new_command(command, settings): diff --git a/thefuck/rules/gulp_not_task.py b/thefuck/rules/gulp_not_task.py index c1a548c1..853fa609 100644 --- a/thefuck/rules/gulp_not_task.py +++ b/thefuck/rules/gulp_not_task.py @@ -1,11 +1,11 @@ import re import subprocess -from thefuck.utils import replace_command +from thefuck.utils import replace_command, for_app +@for_app('gulp') def match(command, script): - return command.script.startswith('gulp')\ - and 'is not in your gulpfile' in command.stdout + return 'is not in your gulpfile' in command.stdout def get_gulp_tasks(): diff --git a/thefuck/rules/heroku_not_command.py b/thefuck/rules/heroku_not_command.py index 87360bc8..a01e1577 100644 --- a/thefuck/rules/heroku_not_command.py +++ b/thefuck/rules/heroku_not_command.py @@ -1,10 +1,10 @@ import re -from thefuck.utils import replace_command +from thefuck.utils import replace_command, for_app +@for_app('heroku') def match(command, settings): - return command.script.startswith('heroku') and \ - 'is not a heroku command' in command.stderr and \ + return 'is not a heroku command' in command.stderr and \ 'Perhaps you meant' in command.stderr diff --git a/thefuck/rules/java.py b/thefuck/rules/java.py index d2852328..f7a33c74 100644 --- a/thefuck/rules/java.py +++ b/thefuck/rules/java.py @@ -1,13 +1,16 @@ -# Fixes common java command mistake -# -# Example: -# > java foo.java -# Error: Could not find or load main class foo.java +"""Fixes common java command mistake + +Example: +> java foo.java +Error: Could not find or load main class foo.java + +""" +from thefuck.utils import for_app +@for_app('java') def match(command, settings): - return (command.script.startswith('java ') - and command.script.endswith('.java')) + return command.script.endswith('.java') def get_new_command(command, settings): diff --git a/thefuck/rules/javac.py b/thefuck/rules/javac.py index 80e6b258..be40a5e7 100644 --- a/thefuck/rules/javac.py +++ b/thefuck/rules/javac.py @@ -1,14 +1,17 @@ -# Appends .java when compiling java files -# -# Example: -# > javac foo -# error: Class names, 'foo', are only accepted if annotation -# processing is explicitly requested +"""Appends .java when compiling java files + +Example: + > javac foo + error: Class names, 'foo', are only accepted if annotation + processing is explicitly requested + +""" +from thefuck.utils import for_app +@for_app('javac') def match(command, settings): - return (command.script.startswith('javac ') - and not command.script.endswith('.java')) + return not command.script.endswith('.java') def get_new_command(command, settings): diff --git a/thefuck/rules/lein_not_task.py b/thefuck/rules/lein_not_task.py index db98c951..3849ac55 100644 --- a/thefuck/rules/lein_not_task.py +++ b/thefuck/rules/lein_not_task.py @@ -1,9 +1,10 @@ import re -from thefuck.utils import replace_command, get_all_matched_commands +from thefuck.utils import replace_command, get_all_matched_commands, for_app from thefuck.specific.sudo import sudo_support @sudo_support +@for_app('lein') def match(command, settings): return (command.script.startswith('lein') and "is not a task. See 'lein help'" in command.stderr diff --git a/thefuck/rules/ls_lah.py b/thefuck/rules/ls_lah.py index 580744bb..b8e6590b 100644 --- a/thefuck/rules/ls_lah.py +++ b/thefuck/rules/ls_lah.py @@ -1,7 +1,9 @@ +from thefuck.utils import for_app + + +@for_app('ls') def match(command, settings): - return (command.script == 'ls' - or command.script.startswith('ls ') - and 'ls -' not in command.script) + return 'ls -' not in command.script def get_new_command(command, settings): diff --git a/thefuck/rules/mercurial.py b/thefuck/rules/mercurial.py index c2e9aa6e..338629aa 100644 --- a/thefuck/rules/mercurial.py +++ b/thefuck/rules/mercurial.py @@ -1,5 +1,5 @@ import re -from thefuck.utils import get_closest +from thefuck.utils import get_closest, for_app def extract_possibilities(command): @@ -12,14 +12,12 @@ def extract_possibilities(command): return possib +@for_app('hg') def match(command, settings): - return (command.script.startswith('hg ') - and ('hg: unknown command' in command.stderr - and '(did you mean one of ' in command.stderr - or "hg: command '" in command.stderr - and "' is ambiguous:" in command.stderr - ) - ) + return ('hg: unknown command' in command.stderr + and '(did you mean one of ' in command.stderr + or "hg: command '" in command.stderr + and "' is ambiguous:" in command.stderr) def get_new_command(command, settings): diff --git a/thefuck/rules/open.py b/thefuck/rules/open.py index 22aaea37..6de2c963 100644 --- a/thefuck/rules/open.py +++ b/thefuck/rules/open.py @@ -5,21 +5,21 @@ # The file ~/github.com does not exist. # Perhaps you meant 'http://github.com'? # +from thefuck.utils import for_app +@for_app('open', 'xdg-open', 'gnome-open', 'kde-open') def match(command, settings): - return (command.script.startswith(('open', 'xdg-open', 'gnome-open', 'kde-open')) - and ( - '.com' in command.script - or '.net' in command.script - or '.org' in command.script - or '.ly' in command.script - or '.io' in command.script - or '.se' in command.script - or '.edu' in command.script - or '.info' in command.script - or '.me' in command.script - or 'www.' in command.script)) + return ('.com' in command.script + or '.net' in command.script + or '.org' in command.script + or '.ly' in command.script + or '.io' in command.script + or '.se' in command.script + or '.edu' in command.script + or '.info' in command.script + or '.me' in command.script + or 'www.' in command.script) def get_new_command(command, settings): diff --git a/thefuck/rules/pip_unknown_command.py b/thefuck/rules/pip_unknown_command.py index 9ae185d3..61293f80 100644 --- a/thefuck/rules/pip_unknown_command.py +++ b/thefuck/rules/pip_unknown_command.py @@ -1,7 +1,10 @@ import re -from thefuck.utils import replace_argument +from thefuck.utils import replace_argument, for_app +from thefuck.specific.sudo import sudo_support +@sudo_support +@for_app('pip') def match(command, settings): return ('pip' in command.script and 'unknown command' in command.stderr and diff --git a/thefuck/rules/python_execute.py b/thefuck/rules/python_execute.py index d4d9d266..2de751e9 100644 --- a/thefuck/rules/python_execute.py +++ b/thefuck/rules/python_execute.py @@ -3,11 +3,12 @@ # Example: # > python foo # error: python: can't open file 'foo': [Errno 2] No such file or directory +from thefuck.utils import for_app +@for_app('python') def match(command, settings): - return (command.script.startswith('python ') - and not command.script.endswith('.py')) + return not command.script.endswith('.py') def get_new_command(command, settings): diff --git a/thefuck/rules/sed_unterminated_s.py b/thefuck/rules/sed_unterminated_s.py index 80334f94..5a8ea6d3 100644 --- a/thefuck/rules/sed_unterminated_s.py +++ b/thefuck/rules/sed_unterminated_s.py @@ -1,10 +1,10 @@ import shlex -from thefuck.utils import quote +from thefuck.utils import quote, for_app +@for_app('sed') def match(command, settings): - return ('sed' in command.script - and "unterminated `s' command" in command.stderr) + return "unterminated `s' command" in command.stderr def get_new_command(command, settings): diff --git a/thefuck/rules/ssh_known_hosts.py b/thefuck/rules/ssh_known_hosts.py index df908d00..b14533bb 100644 --- a/thefuck/rules/ssh_known_hosts.py +++ b/thefuck/rules/ssh_known_hosts.py @@ -1,4 +1,5 @@ import re +from thefuck.utils import for_app patterns = [ r'WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED!', @@ -12,6 +13,7 @@ offending_pattern = re.compile( commands = ['ssh', 'scp'] +@for_app(*commands) def match(command, settings): if not command.script: return False diff --git a/thefuck/rules/systemctl.py b/thefuck/rules/systemctl.py index ef8c0ca1..3edca9fc 100644 --- a/thefuck/rules/systemctl.py +++ b/thefuck/rules/systemctl.py @@ -2,15 +2,16 @@ The confusion in systemctl's param order is massive. """ from thefuck.specific.sudo import sudo_support +from thefuck.utils import for_app @sudo_support +@for_app('systemctl') def match(command, settings): # Catches 'Unknown operation 'service'.' when executing systemctl with # misordered arguments cmd = command.script.split() - return ('systemctl' in command.script and - 'Unknown operation \'' in command.stderr and + return ('Unknown operation \'' in command.stderr and len(cmd) - cmd.index('systemctl') == 3) diff --git a/thefuck/rules/tmux.py b/thefuck/rules/tmux.py index 2ba446e3..09acb578 100644 --- a/thefuck/rules/tmux.py +++ b/thefuck/rules/tmux.py @@ -1,10 +1,10 @@ -from thefuck.utils import replace_command import re +from thefuck.utils import replace_command, for_app +@for_app('tmux') def match(command, settings): - return ('tmux' in command.script - and 'ambiguous command:' in command.stderr + return ('ambiguous command:' in command.stderr and 'could be:' in command.stderr) diff --git a/thefuck/rules/tsuru_login.py b/thefuck/rules/tsuru_login.py index b71803fd..fc917159 100644 --- a/thefuck/rules/tsuru_login.py +++ b/thefuck/rules/tsuru_login.py @@ -1,9 +1,10 @@ from thefuck import shells +from thefuck.utils import for_app +@for_app('tsuru') def match(command, settings): - return (command.script.startswith('tsuru') - and 'not authenticated' in command.stderr + return ('not authenticated' in command.stderr and 'session has expired' in command.stderr) diff --git a/thefuck/rules/tsuru_not_command.py b/thefuck/rules/tsuru_not_command.py index 86b4d15f..498a4930 100644 --- a/thefuck/rules/tsuru_not_command.py +++ b/thefuck/rules/tsuru_not_command.py @@ -1,10 +1,10 @@ import re -from thefuck.utils import get_all_matched_commands, replace_command +from thefuck.utils import get_all_matched_commands, replace_command, for_app +@for_app('tsuru') def match(command, settings): - return (command.script.startswith('tsuru ') - and ' is not a tsuru command. See "tsuru help".' in command.stderr + return (' is not a tsuru command. See "tsuru help".' in command.stderr and '\nDid you mean?\n\t' in command.stderr) diff --git a/thefuck/rules/vagrant_up.py b/thefuck/rules/vagrant_up.py index 9c0a1e40..7830f301 100644 --- a/thefuck/rules/vagrant_up.py +++ b/thefuck/rules/vagrant_up.py @@ -1,8 +1,10 @@ from thefuck import shells +from thefuck.utils import for_app +@for_app('vagrant') def match(command, settings): - return command.script.startswith('vagrant ') and 'run `vagrant up`' in command.stderr.lower() + return 'run `vagrant up`' in command.stderr.lower() def get_new_command(command, settings): diff --git a/thefuck/specific/git.py b/thefuck/specific/git.py index b6cd22ff..4c415987 100644 --- a/thefuck/specific/git.py +++ b/thefuck/specific/git.py @@ -2,21 +2,18 @@ from functools import wraps import re from shlex import split from ..types import Command -from ..utils import quote +from ..utils import quote, for_app def git_support(fn): """Resolves git aliases and supports testing for both git and hub.""" + # supports GitHub's `hub` command + # which is recommended to be used with `alias git=hub` + # but at this point, shell aliases have already been resolved + + @for_app('git', 'hub') @wraps(fn) def wrapper(command, settings): - # supports GitHub's `hub` command - # which is recommended to be used with `alias git=hub` - # but at this point, shell aliases have already been resolved - is_git_cmd = command.script.startswith(('git', 'hub')) - - if not is_git_cmd: - return False - # perform git aliases expansion if 'trace: alias expansion:' in command.stderr: search = re.search("trace: alias expansion: ([^ ]*) => ([^\n]*)", From ebe53f0d181c28ec2f7a86f46d7d51a7d48bbd9e Mon Sep 17 00:00:00 2001 From: nvbn Date: Thu, 27 Aug 2015 16:52:26 +0300 Subject: [PATCH 07/10] Use decorator library --- setup.py | 2 +- tests/specific/test_sudo.py | 6 ++++-- thefuck/specific/git.py | 40 ++++++++++++++++++------------------- thefuck/specific/sudo.py | 32 ++++++++++++++--------------- thefuck/utils.py | 33 ++++++++++++------------------ 5 files changed, 52 insertions(+), 61 deletions(-) diff --git a/setup.py b/setup.py index e2d6e12c..88900cf5 100755 --- a/setup.py +++ b/setup.py @@ -22,7 +22,7 @@ elif (3, 0) < version < (3, 3): VERSION = '2.8' -install_requires = ['psutil', 'colorama', 'six'] +install_requires = ['psutil', 'colorama', 'six', 'decorator'] extras_require = {':python_version<"3.4"': ['pathlib']} setup(name='thefuck', diff --git a/tests/specific/test_sudo.py b/tests/specific/test_sudo.py index 46afa5c2..b8243322 100644 --- a/tests/specific/test_sudo.py +++ b/tests/specific/test_sudo.py @@ -13,6 +13,8 @@ from tests.utils import Command (False, 'sudo ls', 'ls', False), (False, 'ls', 'ls', False)]) def test_sudo_support(return_value, command, called, result): - fn = Mock(return_value=return_value, __name__='') + def fn(command, settings): + assert command == Command(called) + return return_value + assert sudo_support(fn)(Command(command), None) == result - fn.assert_called_once_with(Command(called), None) diff --git a/thefuck/specific/git.py b/thefuck/specific/git.py index 4c415987..b8420573 100644 --- a/thefuck/specific/git.py +++ b/thefuck/specific/git.py @@ -1,34 +1,32 @@ -from functools import wraps import re from shlex import split +from decorator import decorator from ..types import Command -from ..utils import quote, for_app +from ..utils import quote, is_app -def git_support(fn): +@decorator +def git_support(fn, command, settings): """Resolves git aliases and supports testing for both git and hub.""" # supports GitHub's `hub` command # which is recommended to be used with `alias git=hub` # but at this point, shell aliases have already been resolved + if not is_app(command, 'git', 'hub'): + return False - @for_app('git', 'hub') - @wraps(fn) - def wrapper(command, settings): - # perform git aliases expansion - if 'trace: alias expansion:' in command.stderr: - search = re.search("trace: alias expansion: ([^ ]*) => ([^\n]*)", - command.stderr) - alias = search.group(1) + # perform git aliases expansion + if 'trace: alias expansion:' in command.stderr: + search = re.search("trace: alias expansion: ([^ ]*) => ([^\n]*)", + command.stderr) + alias = search.group(1) - # by default git quotes everything, for example: - # 'commit' '--amend' - # which is surprising and does not allow to easily test for - # eg. 'git commit' - expansion = ' '.join(map(quote, split(search.group(2)))) - new_script = command.script.replace(alias, expansion) + # by default git quotes everything, for example: + # 'commit' '--amend' + # which is surprising and does not allow to easily test for + # eg. 'git commit' + expansion = ' '.join(map(quote, split(search.group(2)))) + new_script = command.script.replace(alias, expansion) - command = Command._replace(command, script=new_script) + command = Command._replace(command, script=new_script) - return fn(command, settings) - - return wrapper + return fn(command, settings) diff --git a/thefuck/specific/sudo.py b/thefuck/specific/sudo.py index 88a6cb07..8e5d30cd 100644 --- a/thefuck/specific/sudo.py +++ b/thefuck/specific/sudo.py @@ -1,24 +1,22 @@ -from functools import wraps import six +from decorator import decorator from ..types import Command -def sudo_support(fn): +@decorator +def sudo_support(fn, command, settings): """Removes sudo before calling fn and adds it after.""" - @wraps(fn) - def wrapper(command, settings): - if not command.script.startswith('sudo '): - return fn(command, settings) + if not command.script.startswith('sudo '): + return fn(command, settings) - result = fn(Command(command.script[5:], - command.stdout, - command.stderr), - settings) + result = fn(Command(command.script[5:], + command.stdout, + command.stderr), + settings) - if result and isinstance(result, six.string_types): - return u'sudo {}'.format(result) - elif isinstance(result, list): - return [u'sudo {}'.format(x) for x in result] - else: - return result - return wrapper \ No newline at end of file + if result and isinstance(result, six.string_types): + return u'sudo {}'.format(result) + elif isinstance(result, list): + return [u'sudo {}'.format(x) for x in result] + else: + return result diff --git a/thefuck/utils.py b/thefuck/utils.py index 2887397e..afa110e0 100644 --- a/thefuck/utils.py +++ b/thefuck/utils.py @@ -1,5 +1,6 @@ from difflib import get_close_matches from functools import wraps +from decorator import decorator import os import pickle @@ -47,12 +48,9 @@ def wrap_settings(params): print(settings.apt) """ - def decorator(fn): - @wraps(fn) - def wrapper(command, settings): - return fn(command, settings.update(**params)) - return wrapper - return decorator + def _wrap_settings(fn, command, settings): + return fn(command, settings.update(**params)) + return decorator(_wrap_settings) def memoize(fn): @@ -110,11 +108,9 @@ def replace_argument(script, from_, to): u' {} '.format(from_), u' {} '.format(to), 1) -def eager(fn): - @wraps(fn) - def wrapper(*args, **kwargs): - return list(fn(*args, **kwargs)) - return wrapper +@decorator +def eager(fn, *args, **kwargs): + return list(fn(*args, **kwargs)) @eager @@ -146,13 +142,10 @@ def is_app(command, *app_names): def for_app(*app_names): """Specifies that matching script is for on of app names.""" - def decorator(fn): - @wraps(fn) - def wrapper(command, settings): - if is_app(command, *app_names): - return fn(command, settings) - else: - return False + def _for_app(fn, command, settings): + if is_app(command, *app_names): + return fn(command, settings) + else: + return False - return wrapper - return decorator + return decorator(_for_app) From 12394ca8423a438915fed996383b44471fc1139d Mon Sep 17 00:00:00 2001 From: nvbn Date: Tue, 1 Sep 2015 12:51:41 +0300 Subject: [PATCH 08/10] #334: Don't wait for all rules before showing result --- tests/conftest.py | 6 ++++ tests/test_corrector.py | 13 ++------ tests/test_types.py | 33 ++++++++++++++++-- tests/test_ui.py | 24 ++++++++----- thefuck/corrector.py | 22 ++---------- thefuck/types.py | 74 +++++++++++++++++++++++++++++++++++++++-- thefuck/ui.py | 6 ++-- 7 files changed, 132 insertions(+), 46 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index e7f55e9c..aa232a16 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,6 +1,12 @@ import pytest +from mock import Mock @pytest.fixture def no_memoize(monkeypatch): monkeypatch.setattr('thefuck.utils.memoize.disabled', True) + + +@pytest.fixture +def settings(): + return Mock(debug=False, no_colors=True) diff --git a/tests/test_corrector.py b/tests/test_corrector.py index 0496e61e..7142aa1e 100644 --- a/tests/test_corrector.py +++ b/tests/test_corrector.py @@ -3,7 +3,7 @@ from pathlib import PosixPath, Path from mock import Mock from thefuck import corrector, conf, types from tests.utils import Rule, Command, CorrectedCommand -from thefuck.corrector import make_corrected_commands, get_corrected_commands, remove_duplicates +from thefuck.corrector import make_corrected_commands, get_corrected_commands def test_load_rule(mocker): @@ -75,15 +75,6 @@ class TestGetCorrectedCommands(object): == [CorrectedCommand(script='test!', priority=100)] -def test_remove_duplicates(): - side_effect = lambda *_: None - assert set(remove_duplicates([CorrectedCommand('ls', priority=100), - CorrectedCommand('ls', priority=200), - CorrectedCommand('ls', side_effect, 300)])) \ - == {CorrectedCommand('ls', priority=100), - CorrectedCommand('ls', side_effect, 300)} - - def test_get_corrected_commands(mocker): command = Command('test', 'test', 'test') rules = [Rule(match=lambda *_: False), @@ -94,4 +85,4 @@ def test_get_corrected_commands(mocker): priority=60)] mocker.patch('thefuck.corrector.get_rules', return_value=rules) assert [cmd.script for cmd in get_corrected_commands(command, None, Mock(debug=False))] \ - == ['test@', 'test!', 'test;'] + == ['test!', 'test@', 'test;'] diff --git a/tests/test_types.py b/tests/test_types.py index 41d0f10a..910bbc98 100644 --- a/tests/test_types.py +++ b/tests/test_types.py @@ -1,5 +1,6 @@ -from thefuck.types import RulesNamesList, Settings -from tests.utils import Rule +from thefuck.types import RulesNamesList, Settings, \ + SortedCorrectedCommandsSequence +from tests.utils import Rule, CorrectedCommand def test_rules_names_list(): @@ -15,3 +16,31 @@ def test_update_settings(): assert new_settings.key == 'val' assert new_settings.unset == 'unset-value' assert settings.key == 'val' + + +class TestSortedCorrectedCommandsSequence(object): + def test_realises_generator_only_on_demand(self, settings): + should_realise = False + + def gen(): + nonlocal should_realise + yield CorrectedCommand('git commit') + yield CorrectedCommand('git branch', priority=200) + assert should_realise + yield CorrectedCommand('git checkout', priority=100) + + commands = SortedCorrectedCommandsSequence(gen(), settings) + assert commands[0] == CorrectedCommand('git commit') + should_realise = True + assert commands[1] == CorrectedCommand('git checkout', priority=100) + assert commands[2] == CorrectedCommand('git branch', priority=200) + + def test_remove_duplicates(self, settings): + side_effect = lambda *_: None + seq = SortedCorrectedCommandsSequence( + iter([CorrectedCommand('ls', priority=100), + CorrectedCommand('ls', priority=200), + CorrectedCommand('ls', side_effect, 300)]), + settings) + assert set(seq) == {CorrectedCommand('ls', priority=100), + CorrectedCommand('ls', side_effect, 300)} diff --git a/tests/test_ui.py b/tests/test_ui.py index d822ebe8..f919963e 100644 --- a/tests/test_ui.py +++ b/tests/test_ui.py @@ -4,7 +4,7 @@ from mock import Mock import pytest from itertools import islice from thefuck import ui -from thefuck.types import CorrectedCommand +from thefuck.types import CorrectedCommand, SortedCorrectedCommandsSequence @pytest.fixture @@ -58,14 +58,18 @@ def test_command_selector(): class TestSelectCommand(object): @pytest.fixture - def commands_with_side_effect(self): - return [CorrectedCommand('ls', lambda *_: None, 100), - CorrectedCommand('cd', lambda *_: None, 100)] + def commands_with_side_effect(self, settings): + return SortedCorrectedCommandsSequence( + iter([CorrectedCommand('ls', lambda *_: None, 100), + CorrectedCommand('cd', lambda *_: None, 100)]), + settings) @pytest.fixture - def commands(self): - return [CorrectedCommand('ls', None, 100), - CorrectedCommand('cd', None, 100)] + def commands(self, settings): + return SortedCorrectedCommandsSequence( + iter([CorrectedCommand('ls', None, 100), + CorrectedCommand('cd', None, 100)]), + settings) def test_without_commands(self, capsys): assert ui.select_command([], Mock(debug=False, no_color=True)) is None @@ -92,9 +96,11 @@ class TestSelectCommand(object): require_confirmation=True)) == commands[0] assert capsys.readouterr() == ('', u'\x1b[1K\rls [enter/↑/↓/ctrl+c]\n') - def test_with_confirmation_one_match(self, capsys, patch_getch, commands): + def test_with_confirmation_one_match(self, capsys, patch_getch, commands, + settings): patch_getch(['\n']) - assert ui.select_command((commands[0],), + seq = SortedCorrectedCommandsSequence(iter([commands[0]]), settings) + assert ui.select_command(seq, Mock(debug=False, no_color=True, require_confirmation=True)) == commands[0] assert capsys.readouterr() == ('', u'\x1b[1K\rls [enter/ctrl+c]\n') diff --git a/thefuck/corrector.py b/thefuck/corrector.py index 41713582..981c0fe4 100644 --- a/thefuck/corrector.py +++ b/thefuck/corrector.py @@ -2,7 +2,6 @@ import sys from imp import load_source from pathlib import Path from . import conf, types, logs -from .utils import eager def load_rule(rule, settings): @@ -27,17 +26,16 @@ def get_loaded_rules(rules, settings): yield loaded_rule -@eager def get_rules(user_dir, settings): """Returns all enabled rules.""" bundled = Path(__file__).parent \ .joinpath('rules') \ .glob('*.py') user = user_dir.joinpath('rules').glob('*.py') - return get_loaded_rules(sorted(bundled) + sorted(user), settings) + return sorted(get_loaded_rules(sorted(bundled) + sorted(user), settings), + key=lambda rule: rule.priority) -@eager def get_matched_rules(command, rules, settings): """Returns first matched rule for command.""" script_only = command.stdout is None and command.stderr is None @@ -66,22 +64,8 @@ def make_corrected_commands(command, rules, settings): priority=(n + 1) * rule.priority) -def remove_duplicates(corrected_commands): - commands = {(command.script, command.side_effect): command - for command in sorted(corrected_commands, - key=lambda command: -command.priority)} - return commands.values() - - def get_corrected_commands(command, user_dir, settings): rules = get_rules(user_dir, settings) - logs.debug( - u'Loaded rules: {}'.format(', '.join(rule.name for rule in rules)), - settings) matched = get_matched_rules(command, rules, settings) - logs.debug( - u'Matched rules: {}'.format(', '.join(rule.name for rule in matched)), - settings) corrected_commands = make_corrected_commands(command, matched, settings) - return sorted(remove_duplicates(corrected_commands), - key=lambda corrected_command: corrected_command.priority) + return types.SortedCorrectedCommandsSequence(corrected_commands, settings) diff --git a/thefuck/types.py b/thefuck/types.py index 2e5b41af..6da4234c 100644 --- a/thefuck/types.py +++ b/thefuck/types.py @@ -1,5 +1,6 @@ from collections import namedtuple - +from traceback import format_stack +from .logs import debug Command = namedtuple('Command', ('script', 'stdout', 'stderr')) @@ -18,7 +19,6 @@ class RulesNamesList(list): class Settings(dict): - def __getattr__(self, item): return self.get(item) @@ -29,3 +29,73 @@ class Settings(dict): conf = dict(kwargs) conf.update(self) return Settings(conf) + + +class SortedCorrectedCommandsSequence(object): + """List-like collection/wrapper around generator, that: + + - immediately gives access to the first commands through []; + - realises generator and sorts commands on first access to other + commands through [], or when len called. + + """ + + def __init__(self, commands, settings): + self._settings = settings + self._commands = commands + self._cached = self._get_first_two_unique() + self._realised = False + + def _get_first_two_unique(self): + """Returns first two unique commands.""" + try: + first = next(self._commands) + except StopIteration: + return [] + + for command in self._commands: + if command.script != first.script or \ + command.side_effect != first.side_effect: + return [first, command] + return [first] + + def _remove_duplicates(self, corrected_commands): + """Removes low-priority duplicates.""" + commands = {(command.script, command.side_effect): command + for command in sorted(corrected_commands, + key=lambda command: -command.priority) + if command.script != self._cached[0].script + or command.side_effect != self._cached[0].side_effect} + return commands.values() + + def _realise(self): + """Realises generator, removes duplicates and sorts commands.""" + commands = self._cached[1:] + list(self._commands) + commands = self._remove_duplicates(commands) + self._cached = [self._cached[0]] + sorted( + commands, key=lambda corrected_command: corrected_command.priority) + self._realised = True + debug('SortedCommandsSequence was realised with: {}, after: {}'.format( + self._cached, '\n'.join(format_stack())), self._settings) + + def __getitem__(self, item): + if item != 0 and not self._realised: + self._realise() + return self._cached[item] + + def __bool__(self): + return bool(self._cached) + + def __len__(self): + if not self._realised: + self._realise() + return len(self._cached) + + def __iter__(self): + if not self._realised: + self._realise() + return iter(self._cached) + + @property + def is_multiple(self): + return len(self._cached) > 1 diff --git a/thefuck/ui.py b/thefuck/ui.py index 146cbd73..31f0316f 100644 --- a/thefuck/ui.py +++ b/thefuck/ui.py @@ -88,9 +88,9 @@ def select_command(corrected_commands, settings): logs.show_corrected_command(selector.value, settings) return selector.value - multiple_cmds = len(corrected_commands) > 1 - - selector.on_change(lambda val: logs.confirm_text(val, multiple_cmds, settings)) + selector.on_change( + lambda val: logs.confirm_text(val, corrected_commands.is_multiple, + settings)) for action in read_actions(): if action == SELECT: sys.stderr.write('\n') From 5d74344994da89ed01afd448f1c9d86b85e85351 Mon Sep 17 00:00:00 2001 From: nvbn Date: Tue, 1 Sep 2015 13:03:24 +0300 Subject: [PATCH 09/10] Make `CorrectedCommand` ignore priority when checking equality --- tests/test_types.py | 13 +++++++++++++ thefuck/types.py | 33 +++++++++++++++++++++++++-------- 2 files changed, 38 insertions(+), 8 deletions(-) diff --git a/tests/test_types.py b/tests/test_types.py index 910bbc98..c17f7a58 100644 --- a/tests/test_types.py +++ b/tests/test_types.py @@ -44,3 +44,16 @@ class TestSortedCorrectedCommandsSequence(object): settings) assert set(seq) == {CorrectedCommand('ls', priority=100), CorrectedCommand('ls', side_effect, 300)} + + +class TestCorrectedCommand(object): + + def test_equality(self): + assert CorrectedCommand('ls', None, 100) == \ + CorrectedCommand('ls', None, 200) + assert CorrectedCommand('ls', None, 100) != \ + CorrectedCommand('ls', lambda *_: _, 100) + + def test_hashable(self): + assert {CorrectedCommand('ls', None, 100), + CorrectedCommand('ls', None, 200)} == {CorrectedCommand('ls')} diff --git a/thefuck/types.py b/thefuck/types.py index 6da4234c..3f2155b6 100644 --- a/thefuck/types.py +++ b/thefuck/types.py @@ -4,12 +4,31 @@ from .logs import debug Command = namedtuple('Command', ('script', 'stdout', 'stderr')) -CorrectedCommand = namedtuple('CorrectedCommand', ('script', 'side_effect', 'priority')) - Rule = namedtuple('Rule', ('name', 'match', 'get_new_command', 'enabled_by_default', 'side_effect', 'priority', 'requires_output')) +class CorrectedCommand(object): + def __init__(self, script, side_effect, priority): + self.script = script + self.side_effect = side_effect + self.priority = priority + + def __eq__(self, other): + """Ignores `priority` field.""" + if isinstance(other, CorrectedCommand): + return (other.script, other.side_effect) ==\ + (self.script, self.side_effect) + else: + return False + + def __hash__(self): + return (self.script, self.side_effect).__hash__() + + def __repr__(self): + return 'CorrectedCommand(script={}, side_effect={}, priority={})'.format( + self.script, self.side_effect, self.priority) + class RulesNamesList(list): """Wrapper a top of list for storing rules names.""" @@ -54,19 +73,17 @@ class SortedCorrectedCommandsSequence(object): return [] for command in self._commands: - if command.script != first.script or \ - command.side_effect != first.side_effect: + if command != first: return [first, command] return [first] def _remove_duplicates(self, corrected_commands): """Removes low-priority duplicates.""" - commands = {(command.script, command.side_effect): command + commands = {command for command in sorted(corrected_commands, key=lambda command: -command.priority) - if command.script != self._cached[0].script - or command.side_effect != self._cached[0].side_effect} - return commands.values() + if command.script != self._cached[0]} + return commands def _realise(self): """Realises generator, removes duplicates and sorts commands.""" From 61937e9e8f2a35ab9588b4973ec29f11da54a9bd Mon Sep 17 00:00:00 2001 From: nvbn Date: Tue, 1 Sep 2015 14:34:41 +0300 Subject: [PATCH 10/10] #334: Wait only for first matched rule; regression: always show arrows --- tests/test_ui.py | 9 --------- thefuck/logs.py | 10 +++------- thefuck/types.py | 19 ++++--------------- thefuck/ui.py | 4 +--- 4 files changed, 8 insertions(+), 34 deletions(-) diff --git a/tests/test_ui.py b/tests/test_ui.py index f919963e..f997374c 100644 --- a/tests/test_ui.py +++ b/tests/test_ui.py @@ -96,15 +96,6 @@ class TestSelectCommand(object): require_confirmation=True)) == commands[0] assert capsys.readouterr() == ('', u'\x1b[1K\rls [enter/↑/↓/ctrl+c]\n') - def test_with_confirmation_one_match(self, capsys, patch_getch, commands, - settings): - patch_getch(['\n']) - seq = SortedCorrectedCommandsSequence(iter([commands[0]]), settings) - assert ui.select_command(seq, - Mock(debug=False, no_color=True, - require_confirmation=True)) == commands[0] - assert capsys.readouterr() == ('', u'\x1b[1K\rls [enter/ctrl+c]\n') - def test_with_confirmation_abort(self, capsys, patch_getch, commands): patch_getch([KeyboardInterrupt]) assert ui.select_command(commands, diff --git a/thefuck/logs.py b/thefuck/logs.py index 8c7b3a7d..ae5f1ad2 100644 --- a/thefuck/logs.py +++ b/thefuck/logs.py @@ -45,15 +45,11 @@ def show_corrected_command(corrected_command, settings): reset=color(colorama.Style.RESET_ALL, settings))) -def confirm_text(corrected_command, multiple_cmds, settings): - if multiple_cmds: - arrows = '{blue}↑{reset}/{blue}↓{reset}/' - else: - arrows = '' - +def confirm_text(corrected_command, settings): sys.stderr.write( ('{clear}{bold}{script}{reset}{side_effect} ' - '[{green}enter{reset}/' + arrows + '{red}ctrl+c{reset}]').format( + '[{green}enter{reset}/{blue}↑{reset}/{blue}↓{reset}' + '/{red}ctrl+c{reset}]').format( script=corrected_command.script, side_effect=' (+side effect)' if corrected_command.side_effect else '', clear='\033[1K\r', diff --git a/thefuck/types.py b/thefuck/types.py index 3f2155b6..69b174ea 100644 --- a/thefuck/types.py +++ b/thefuck/types.py @@ -62,21 +62,15 @@ class SortedCorrectedCommandsSequence(object): def __init__(self, commands, settings): self._settings = settings self._commands = commands - self._cached = self._get_first_two_unique() + self._cached = self._realise_first() self._realised = False - def _get_first_two_unique(self): - """Returns first two unique commands.""" + def _realise_first(self): try: - first = next(self._commands) + return [next(self._commands)] except StopIteration: return [] - for command in self._commands: - if command != first: - return [first, command] - return [first] - def _remove_duplicates(self, corrected_commands): """Removes low-priority duplicates.""" commands = {command @@ -87,8 +81,7 @@ class SortedCorrectedCommandsSequence(object): def _realise(self): """Realises generator, removes duplicates and sorts commands.""" - commands = self._cached[1:] + list(self._commands) - commands = self._remove_duplicates(commands) + commands = self._remove_duplicates(self._commands) self._cached = [self._cached[0]] + sorted( commands, key=lambda corrected_command: corrected_command.priority) self._realised = True @@ -112,7 +105,3 @@ class SortedCorrectedCommandsSequence(object): if not self._realised: self._realise() return iter(self._cached) - - @property - def is_multiple(self): - return len(self._cached) > 1 diff --git a/thefuck/ui.py b/thefuck/ui.py index 31f0316f..36dced97 100644 --- a/thefuck/ui.py +++ b/thefuck/ui.py @@ -88,9 +88,7 @@ def select_command(corrected_commands, settings): logs.show_corrected_command(selector.value, settings) return selector.value - selector.on_change( - lambda val: logs.confirm_text(val, corrected_commands.is_multiple, - settings)) + selector.on_change(lambda val: logs.confirm_text(val, settings)) for action in read_actions(): if action == SELECT: sys.stderr.write('\n')