diff --git a/README.md b/README.md index 3ea406fc..464a5c39 100644 --- a/README.md +++ b/README.md @@ -288,6 +288,7 @@ following rules are enabled by default: * `ifconfig_device_not_found` – fixes wrong device names like `wlan0` to `wlp2s0`; * `java` – removes `.java` extension when running Java programs; * `javac` – appends missing `.java` when compiling Java files; +* `kedro_no_such_command` – fixes wrong `kedro` commands like `kedro ne`; * `lein_not_task` – fixes wrong `lein` tasks like `lein rpl`; * `long_form_help` – changes `-h` to `--help` when the short form version is not supported * `ln_no_hard_link` – catches hard link creation on directories, suggest symbolic link; diff --git a/tests/rules/test_kedro_no_such_command.py b/tests/rules/test_kedro_no_such_command.py new file mode 100644 index 00000000..40dc89ad --- /dev/null +++ b/tests/rules/test_kedro_no_such_command.py @@ -0,0 +1,65 @@ +import textwrap +from collections import namedtuple + +import pytest + +from thefuck.types import Command +from thefuck.rules.kedro_no_such_command import match, get_new_command + +USAGE_ERROR_MESSAGE = """\ +Usage: kedro [OPTIONS] COMMAND [ARGS]... +Try 'kedro -h' for help. + +Error: No such command '{broken_cmd}'. +""" + +CommandSuggestions = namedtuple( + 'CommandSuggestions', ['broken_cmd', 'new_cmds', 'args'], defaults=([],) +) + + +@pytest.fixture +def command(request): + script = ' '.join(['kedro', request.param.broken_cmd, *request.param.args]) + output = USAGE_ERROR_MESSAGE.format(broken_cmd=request.param.broken_cmd) + + if not request.param.new_cmds: + return Command(script, output) + + if len(request.param.new_cmds) == 1: + suggestion = '\n\nDid you mean this?' + else: + suggestion = '\n\nDid you mean one of these?\n' + suggestion += textwrap.indent('\n'.join(request.param.new_cmds), ' ' * 4) + return Command(script, output + suggestion) + + +@pytest.mark.parametrize('command', [ + CommandSuggestions('ne', ['new'], ['--starter', 'spaceflights']), + CommandSuggestions('build', ['build-reqs', 'build-docs']), + CommandSuggestions('lin', ['lint', 'info', 'pipeline']), + CommandSuggestions('pipline', ['pipeline', 'lint'], ['create', 'data_processing']), +], indirect=True) +def test_match(command): + assert match(command) + + +@pytest.mark.parametrize('command', [ + CommandSuggestions('create', []), +], indirect=True) +def test_not_match(command): + assert not match(command) + + +@pytest.mark.parametrize('command, result', [ + (CommandSuggestions('ne', ['new'], ['--starter', 'spaceflights']), + ['kedro new --starter spaceflights']), + (CommandSuggestions('build', ['build-reqs', 'build-docs']), + ['kedro build-reqs', 'kedro build-docs']), + (CommandSuggestions('lin', ['lint', 'info', 'pipeline']), + ['kedro lint', 'kedro info', 'kedro pipeline']), + (CommandSuggestions('pipline', ['pipeline', 'lint'], ['create', 'data_processing']), + ['kedro pipeline create data_processing', 'kedro lint create data_processing']), +], indirect=['command']) +def test_get_new_command(command, result): + assert get_new_command(command) == result diff --git a/tests/rules/test_tsuru_not_command.py b/tests/rules/test_tsuru_not_command.py index 9c918543..a838105f 100644 --- a/tests/rules/test_tsuru_not_command.py +++ b/tests/rules/test_tsuru_not_command.py @@ -6,25 +6,25 @@ from thefuck.rules.tsuru_not_command import match, get_new_command @pytest.mark.parametrize('command', [ Command('tsuru log', ( - 'tsuru: "tchururu" is not a tsuru command. See "tsuru help".\n' + 'tsuru: "log" is not a tsuru command. See "tsuru help".\n' '\nDid you mean?\n' '\tapp-log\n' '\tlogin\n' '\tlogout\n' )), Command('tsuru app-l', ( - 'tsuru: "tchururu" is not a tsuru command. See "tsuru help".\n' + 'tsuru: "app-l" is not a tsuru command. See "tsuru help".\n' '\nDid you mean?\n' '\tapp-list\n' '\tapp-log\n' )), Command('tsuru user-list', ( - 'tsuru: "tchururu" is not a tsuru command. See "tsuru help".\n' + 'tsuru: "user-list" is not a tsuru command. See "tsuru help".\n' '\nDid you mean?\n' '\tteam-user-list\n' )), Command('tsuru targetlist', ( - 'tsuru: "tchururu" is not a tsuru command. See "tsuru help".\n' + 'tsuru: "targetlist" is not a tsuru command. See "tsuru help".\n' '\nDid you mean?\n' '\ttarget-list\n' )), diff --git a/thefuck/rules/fab_command_not_found.py b/thefuck/rules/fab_command_not_found.py index 05d6df0c..40b12c6d 100644 --- a/thefuck/rules/fab_command_not_found.py +++ b/thefuck/rules/fab_command_not_found.py @@ -6,7 +6,7 @@ def match(command): return 'Warning: Command(s) not found:' in command.output -# We need different behavior then in get_all_matched_commands. +# We need different behavior than in get_all_matched_commands. @eager def _get_between(content, start, end=None): should_yield = False diff --git a/thefuck/rules/kedro_no_such_command.py b/thefuck/rules/kedro_no_such_command.py new file mode 100644 index 00000000..3c3da96a --- /dev/null +++ b/thefuck/rules/kedro_no_such_command.py @@ -0,0 +1,29 @@ +import re + +from thefuck.utils import for_app, get_all_matched_commands, replace_argument + + +@for_app('kedro') +def match(command): + return 'No such command ' in command.output and 'Did you mean ' in command.output + + +def _get_single_matched_command(stderr): + """Matches one suggestion found on the same line as 'Did you mean '. + + If Kedro only finds one match, it prints it on the same line instead + of on a new line. This won't work with ``get_all_matched_commands``. + + """ + return re.findall(r'Did you mean this\?\s*(\w+)', stderr) + + +def get_new_command(command): + broken_cmd = re.findall(r"No such command '([^']*)'.", command.output)[0] + new_cmds = (_get_single_matched_command(command.output) + or get_all_matched_commands(command.output)) + + # Kedro already uses `difflib.get_close_matches` when suggesting CLI + # commands, so we don't want `replace_command` to do so differently. + return [replace_argument(command.script, broken_cmd, new_cmd) + for new_cmd in new_cmds] diff --git a/thefuck/utils.py b/thefuck/utils.py index 466e4ba5..e3c6dc89 100644 --- a/thefuck/utils.py +++ b/thefuck/utils.py @@ -98,7 +98,7 @@ def get_closest(word, possibilities, cutoff=0.6, fallback_to_first=True): def get_close_matches(word, possibilities, n=None, cutoff=0.6): - """Overrides `difflib.get_close_match` to control argument `n`.""" + """Overrides `difflib.get_close_matches` to control argument `n`.""" if n is None: n = settings.num_close_matches return difflib_get_close_matches(word, possibilities, n, cutoff)