From 7933e963d86ec0352f9f575036296d69abcb7b0c Mon Sep 17 00:00:00 2001 From: nvbn Date: Tue, 28 Jul 2015 22:04:27 +0300 Subject: [PATCH 01/14] #298 Add ability to chose matched rule --- README.md | 2 +- setup.py | 2 +- tests/test_corrector.py | 88 ++++++++++++++++++++++++++ tests/test_main.py | 136 +--------------------------------------- tests/test_ui.py | 115 +++++++++++++++++++++++++++++++++ thefuck/corrector.py | 77 +++++++++++++++++++++++ thefuck/logs.py | 42 ++++++------- thefuck/main.py | 94 ++++----------------------- thefuck/types.py | 2 + thefuck/ui.py | 88 ++++++++++++++++++++++++++ 10 files changed, 407 insertions(+), 239 deletions(-) create mode 100644 tests/test_corrector.py create mode 100644 tests/test_ui.py create mode 100644 thefuck/corrector.py create mode 100644 thefuck/ui.py diff --git a/README.md b/README.md index 8a6c8c1a..f948e9ac 100644 --- a/README.md +++ b/README.md @@ -212,7 +212,7 @@ in `~/.thefuck/rules`. The rule should contain two functions: ```python match(command: Command, settings: Settings) -> bool -get_new_command(command: Command, settings: Settings) -> str +get_new_command(command: Command, settings: Settings) -> str | list[str] ``` Also the rule can contain an optional function `side_effect(command: Command, settings: Settings) -> None` diff --git a/setup.py b/setup.py index bdfcbf68..dadcb36a 100755 --- a/setup.py +++ b/setup.py @@ -22,7 +22,7 @@ elif (3, 0) < version < (3, 3): VERSION = '2.5.6' -install_requires = ['psutil', 'colorama', 'six'] +install_requires = ['psutil', 'colorama', 'six', 'getch'] extras_require = {':python_version<"3.4"': ['pathlib']} setup(name='thefuck', diff --git a/tests/test_corrector.py b/tests/test_corrector.py new file mode 100644 index 00000000..cfbc3fda --- /dev/null +++ b/tests/test_corrector.py @@ -0,0 +1,88 @@ +import pytest +from pathlib import PosixPath, Path +from mock import Mock +from thefuck import corrector, conf, types +from tests.utils import Rule, Command +from thefuck.corrector import make_corrected_commands, get_corrected_commands + + +def test_load_rule(mocker): + match = object() + get_new_command = object() + load_source = mocker.patch( + 'thefuck.corrector.load_source', + return_value=Mock(match=match, + get_new_command=get_new_command, + enabled_by_default=True, + priority=900, + requires_output=True)) + assert corrector.load_rule(Path('/rules/bash.py'), settings=Mock(priority={})) \ + == Rule('bash', match, get_new_command, priority=900) + load_source.assert_called_once_with('bash', '/rules/bash.py') + + +class TestGetRules(object): + @pytest.fixture(autouse=True) + def glob(self, mocker): + return mocker.patch('thefuck.corrector.Path.glob', return_value=[]) + + def _compare_names(self, rules, names): + return [r.name for r in rules] == names + + @pytest.mark.parametrize('conf_rules, rules', [ + (conf.DEFAULT_RULES, ['bash', 'lisp', 'bash', 'lisp']), + (types.RulesNamesList(['bash']), ['bash', 'bash'])]) + def test_get(self, monkeypatch, glob, conf_rules, rules): + glob.return_value = [PosixPath('bash.py'), PosixPath('lisp.py')] + monkeypatch.setattr('thefuck.corrector.load_source', + lambda x, _: Rule(x)) + assert self._compare_names( + corrector.get_rules(Path('~'), Mock(rules=conf_rules, priority={})), + rules) + + +class TestGetMatchedRules(object): + def test_no_match(self): + assert list(corrector.get_matched_rules( + Command('ls'), [Rule('', lambda *_: False)], + Mock(no_colors=True))) == [] + + def test_match(self): + rule = Rule('', lambda x, _: x.script == 'cd ..') + assert list(corrector.get_matched_rules( + Command('cd ..'), [rule], Mock(no_colors=True))) == [rule] + + def test_when_rule_failed(self, capsys): + all(corrector.get_matched_rules( + Command('ls'), [Rule('test', Mock(side_effect=OSError('Denied')), + requires_output=False)], + Mock(no_colors=True, debug=False))) + assert capsys.readouterr()[1].split('\n')[0] == '[WARN] Rule test:' + + +class TestGetCorrectedCommands(object): + def test_with_rule_returns_list(self): + rule = Rule(get_new_command=lambda x, _: [x.script + '!', x.script + '@'], + priority=100) + assert list(make_corrected_commands(Command(script='test'), [rule], None)) \ + == [types.CorrectedCommand(script='test!', priority=100, side_effect=None), + types.CorrectedCommand(script='test@', priority=200, side_effect=None)] + + def test_with_rule_returns_command(self): + rule = Rule(get_new_command=lambda x, _: x.script + '!', + priority=100) + assert list(make_corrected_commands(Command(script='test'), [rule], None)) \ + == [types.CorrectedCommand(script='test!', priority=100, side_effect=None)] + + +def test_get_corrected_commands(mocker): + command = Command('test', 'test', 'test') + rules = [Rule(match=lambda *_: False), + Rule(match=lambda *_: True, + get_new_command=lambda x, _: x.script + '!', priority=100), + Rule(match=lambda *_: True, + get_new_command=lambda x, _: [x.script + '@', x.script + ';'], + 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;'] diff --git a/tests/test_main.py b/tests/test_main.py index 05858256..4d66e7ee 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -1,61 +1,8 @@ import pytest from subprocess import PIPE -from pathlib import PosixPath, Path from mock import Mock -from thefuck import main, conf, types -from tests.utils import Rule, Command - - -def test_load_rule(mocker): - match = object() - get_new_command = object() - load_source = mocker.patch( - 'thefuck.main.load_source', - return_value=Mock(match=match, - get_new_command=get_new_command, - enabled_by_default=True, - priority=900, - requires_output=True)) - assert main.load_rule(Path('/rules/bash.py')) \ - == Rule('bash', match, get_new_command, priority=900) - load_source.assert_called_once_with('bash', '/rules/bash.py') - - -class TestGetRules(object): - @pytest.fixture(autouse=True) - def glob(self, mocker): - return mocker.patch('thefuck.main.Path.glob', return_value=[]) - - def _compare_names(self, rules, names): - return [r.name for r in rules] == names - - @pytest.mark.parametrize('conf_rules, rules', [ - (conf.DEFAULT_RULES, ['bash', 'lisp', 'bash', 'lisp']), - (types.RulesNamesList(['bash']), ['bash', 'bash'])]) - def test_get(self, monkeypatch, glob, conf_rules, rules): - glob.return_value = [PosixPath('bash.py'), PosixPath('lisp.py')] - monkeypatch.setattr('thefuck.main.load_source', - lambda x, _: Rule(x)) - assert self._compare_names( - main.get_rules(Path('~'), Mock(rules=conf_rules, priority={})), - rules) - - @pytest.mark.parametrize('priority, unordered, ordered', [ - ({}, - [Rule('bash', priority=100), Rule('python', priority=5)], - ['python', 'bash']), - ({}, - [Rule('lisp', priority=9999), Rule('c', priority=conf.DEFAULT_PRIORITY)], - ['c', 'lisp']), - ({'python': 9999}, - [Rule('bash', priority=100), Rule('python', priority=5)], - ['bash', 'python'])]) - def test_ordered_by_priority(self, monkeypatch, priority, unordered, ordered): - monkeypatch.setattr('thefuck.main._get_loaded_rules', - lambda *_: unordered) - assert self._compare_names( - main.get_rules(Path('~'), Mock(priority=priority)), - ordered) +from thefuck import main +from tests.utils import Command class TestGetCommand(object): @@ -79,7 +26,7 @@ class TestGetCommand(object): def test_get_command_calls(self, Popen): assert main.get_command(Mock(env={}), - ['thefuck', 'apt-get', 'search', 'vim']) \ + ['thefuck', 'apt-get', 'search', 'vim']) \ == Command('apt-get search vim', 'stdout', 'stderr') Popen.assert_called_once_with('apt-get search vim', shell=True, @@ -95,80 +42,3 @@ class TestGetCommand(object): assert main.get_command(Mock(env={}), args).script == result else: assert main.get_command(Mock(env={}), args) is None - - -class TestGetMatchedRule(object): - def test_no_match(self): - assert main.get_matched_rule( - Command('ls'), [Rule('', lambda *_: False)], - Mock(no_colors=True)) is None - - def test_match(self): - rule = Rule('', lambda x, _: x.script == 'cd ..') - assert main.get_matched_rule( - Command('cd ..'), [rule], Mock(no_colors=True)) == rule - - def test_when_rule_failed(self, capsys): - main.get_matched_rule( - Command('ls'), [Rule('test', Mock(side_effect=OSError('Denied')))], - Mock(no_colors=True, debug=False)) - assert capsys.readouterr()[1].split('\n')[0] == '[WARN] Rule test:' - - -class TestRunRule(object): - @pytest.fixture(autouse=True) - def confirm(self, mocker): - return mocker.patch('thefuck.main.confirm', return_value=True) - - def test_run_rule(self, capsys): - main.run_rule(Rule(get_new_command=lambda *_: 'new-command'), - Command(), None) - assert capsys.readouterr() == ('new-command\n', '') - - def test_run_rule_with_side_effect(self, capsys): - side_effect = Mock() - settings = Mock(debug=False) - command = Command() - main.run_rule(Rule(get_new_command=lambda *_: 'new-command', - side_effect=side_effect), - command, settings) - assert capsys.readouterr() == ('new-command\n', '') - side_effect.assert_called_once_with(command, settings) - - def test_when_not_comfirmed(self, capsys, confirm): - confirm.return_value = False - main.run_rule(Rule(get_new_command=lambda *_: 'new-command'), - Command(), None) - assert capsys.readouterr() == ('', '') - - -class TestConfirm(object): - @pytest.fixture - def stdin(self, mocker): - return mocker.patch('sys.stdin.read', return_value='\n') - - def test_when_not_required(self, capsys): - assert main.confirm('command', None, Mock(require_confirmation=False)) - assert capsys.readouterr() == ('', 'command\n') - - def test_with_side_effect_and_without_confirmation(self, capsys): - assert main.confirm('command', Mock(), Mock(require_confirmation=False)) - assert capsys.readouterr() == ('', 'command (+side effect)\n') - - # `stdin` fixture should be applied after `capsys` - def test_when_confirmation_required_and_confirmed(self, capsys, stdin): - assert main.confirm('command', None, Mock(require_confirmation=True, - no_colors=True)) - assert capsys.readouterr() == ('', 'command [enter/ctrl+c]') - - # `stdin` fixture should be applied after `capsys` - def test_when_confirmation_required_and_confirmed_with_side_effect(self, capsys, stdin): - assert main.confirm('command', Mock(), Mock(require_confirmation=True, - no_colors=True)) - assert capsys.readouterr() == ('', 'command (+side effect) [enter/ctrl+c]') - - def test_when_confirmation_required_and_aborted(self, capsys, stdin): - stdin.side_effect = KeyboardInterrupt - assert not main.confirm('command', None, Mock(require_confirmation=True, - no_colors=True)) - assert capsys.readouterr() == ('', 'command [enter/ctrl+c]Aborted\n') diff --git a/tests/test_ui.py b/tests/test_ui.py new file mode 100644 index 00000000..3d7d0186 --- /dev/null +++ b/tests/test_ui.py @@ -0,0 +1,115 @@ +from mock import Mock +import pytest +from itertools import islice +from thefuck import ui +from thefuck.types import CorrectedCommand + + +@pytest.fixture +def patch_getch(monkeypatch): + def patch(vals): + def getch(): + for val in vals: + if val == KeyboardInterrupt: + raise val + else: + yield val + + getch_gen = getch() + monkeypatch.setattr('thefuck.ui.getch', lambda: next(getch_gen)) + + return patch + + +def test_read_actions(patch_getch): + patch_getch([ # Enter: + '\n', + # Enter: + '\r', + # Ignored: + 'x', 'y', + # Up: + '\x1b', '[', 'A', + # Down: + '\x1b', '[', 'B', + # Ctrl+C: + KeyboardInterrupt], ) + assert list(islice(ui.read_actions(), 5)) \ + == [ui.SELECT, ui.SELECT, ui.PREVIOUS, ui.NEXT, ui.ABORT] + + +def test_command_selector(): + selector = ui.CommandSelector([1, 2, 3]) + assert selector.value == 1 + changes = [] + selector.on_change(changes.append) + selector.next() + assert selector.value == 2 + selector.next() + assert selector.value == 3 + selector.next() + assert selector.value == 1 + selector.previous() + assert selector.value == 3 + assert changes == [1, 2, 3, 1, 3] + + +class TestSelectCommand(object): + @pytest.fixture + def commands_with_side_effect(self): + return [CorrectedCommand('ls', lambda *_: None, 100), + CorrectedCommand('cd', lambda *_: None, 100)] + + @pytest.fixture + def commands(self): + return [CorrectedCommand('ls', None, 100), + CorrectedCommand('cd', None, 100)] + + def test_without_commands(self, capsys): + assert ui.select_command([], Mock(debug=False, no_color=True)) is None + assert capsys.readouterr() == ('', 'No fuck given\n') + + def test_without_confirmation(self, capsys, commands): + assert ui.select_command(commands, + Mock(debug=False, no_color=True, + require_confirmation=False)) == commands[0] + assert capsys.readouterr() == ('', 'ls\n') + + def test_without_confirmation_with_side_effects(self, capsys, + commands_with_side_effect): + assert ui.select_command(commands_with_side_effect, + Mock(debug=False, no_color=True, + require_confirmation=False)) \ + == commands_with_side_effect[0] + assert capsys.readouterr() == ('', 'ls (+side effect)\n') + + def test_with_confirmation(self, capsys, patch_getch, commands): + patch_getch(['\n']) + assert ui.select_command(commands, + Mock(debug=False, no_color=True, + require_confirmation=True)) == commands[0] + assert capsys.readouterr() == ('', '\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, + Mock(debug=False, no_color=True, + require_confirmation=True)) is None + assert capsys.readouterr() == ('', '\x1b[1K\rls [enter/↑/↓/ctrl+c]\nAborted\n') + + def test_with_confirmation_with_side_effct(self, capsys, patch_getch, + commands_with_side_effect): + patch_getch(['\n']) + assert ui.select_command(commands_with_side_effect, + Mock(debug=False, no_color=True, + require_confirmation=True))\ + == commands_with_side_effect[0] + assert capsys.readouterr() == ('', '\x1b[1K\rls (+side effect) [enter/↑/↓/ctrl+c]\n') + + def test_with_confirmation_select_second(self, capsys, patch_getch, commands): + patch_getch(['\x1b', '[', 'B', '\n']) + assert ui.select_command(commands, + Mock(debug=False, no_color=True, + require_confirmation=True)) == commands[1] + assert capsys.readouterr() == ( + '', '\x1b[1K\rls [enter/↑/↓/ctrl+c]\x1b[1K\rcd [enter/↑/↓/ctrl+c]\n') diff --git a/thefuck/corrector.py b/thefuck/corrector.py new file mode 100644 index 00000000..d9bef851 --- /dev/null +++ b/thefuck/corrector.py @@ -0,0 +1,77 @@ +from imp import load_source +from pathlib import Path +from . import conf, types, logs +import sys + + +def load_rule(rule, settings): + """Imports rule module and returns it.""" + name = rule.name[:-3] + rule_module = load_source(name, str(rule)) + priority = getattr(rule_module, 'priority', conf.DEFAULT_PRIORITY) + return types.Rule(name, rule_module.match, + rule_module.get_new_command, + getattr(rule_module, 'enabled_by_default', True), + getattr(rule_module, 'side_effect', None), + settings.priority.get(name, priority), + getattr(rule_module, 'requires_output', True)) + + +def get_loaded_rules(rules, settings): + """Yields all available rules.""" + for rule in rules: + if rule.name != '__init__.py': + loaded_rule = load_rule(rule, settings) + if loaded_rule in settings.rules: + yield loaded_rule + + +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) + + +def get_matched_rules(command, rules, settings): + """Returns first matched rule for command.""" + script_only = command.stdout is None and command.stderr is None + + for rule in rules: + if script_only and rule.requires_output: + continue + + try: + with logs.debug_time(u'Trying rule: {};'.format(rule.name), + settings): + if rule.match(command, settings): + yield rule + except Exception: + logs.rule_failed(rule, sys.exc_info(), settings) + + +def make_corrected_commands(command, rules, settings): + for rule in rules: + new_commands = rule.get_new_command(command, settings) + if not isinstance(new_commands, list): + new_commands = [new_commands] + for n, new_command in enumerate(new_commands): + yield types.CorrectedCommand(script=new_command, + side_effect=rule.side_effect, + priority=(n + 1) * rule.priority) + + +def get_corrected_commands(command, user_dir, settings): + rules = list(get_rules(user_dir, settings)) + logs.debug( + u'Loaded rules: {}'.format(', '.join(rule.name for rule in rules)), + settings) + matched = list(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(corrected_commands, + key=lambda corrected_command: corrected_command.priority) diff --git a/thefuck/logs.py b/thefuck/logs.py index 9df6663c..fcdbeca2 100644 --- a/thefuck/logs.py +++ b/thefuck/logs.py @@ -28,27 +28,6 @@ def rule_failed(rule, exc_info, settings): exception('Rule {}'.format(rule.name), exc_info, settings) -def show_command(new_command, side_effect, settings): - sys.stderr.write('{bold}{command}{reset}{side_effect}\n'.format( - command=new_command, - side_effect=' (+side effect)' if side_effect else '', - bold=color(colorama.Style.BRIGHT, settings), - reset=color(colorama.Style.RESET_ALL, settings))) - - -def confirm_command(new_command, side_effect, settings): - sys.stderr.write( - '{bold}{command}{reset}{side_effect} ' - '[{green}enter{reset}/{red}ctrl+c{reset}]'.format( - command=new_command, - side_effect=' (+side effect)' if side_effect else '', - bold=color(colorama.Style.BRIGHT, settings), - green=color(colorama.Fore.GREEN, settings), - red=color(colorama.Fore.RED, settings), - reset=color(colorama.Style.RESET_ALL, settings))) - sys.stderr.flush() - - def failed(msg, settings): sys.stderr.write('{red}{msg}{reset}\n'.format( msg=msg, @@ -56,6 +35,27 @@ def failed(msg, settings): reset=color(colorama.Style.RESET_ALL, settings))) +def show_corrected_command(corrected_command, settings): + sys.stderr.write('{bold}{script}{reset}{side_effect}\n'.format( + script=corrected_command.script, + side_effect=' (+side effect)' if corrected_command.side_effect else '', + bold=color(colorama.Style.BRIGHT, settings), + reset=color(colorama.Style.RESET_ALL, settings))) + + +def confirm_text(corrected_command, settings): + sys.stderr.write( + '\033[1K\r{bold}{script}{reset}{side_effect} ' + '[{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 '', + bold=color(colorama.Style.BRIGHT, settings), + green=color(colorama.Fore.GREEN, settings), + red=color(colorama.Fore.RED, settings), + reset=color(colorama.Style.RESET_ALL, settings), + blue=color(colorama.Fore.BLUE, settings))) + + def debug(msg, settings): if settings.debug: sys.stderr.write(u'{blue}{bold}DEBUG:{reset} {msg}\n'.format( diff --git a/thefuck/main.py b/thefuck/main.py index 8ea1f82b..c5f4113a 100644 --- a/thefuck/main.py +++ b/thefuck/main.py @@ -1,4 +1,3 @@ -from imp import load_source from pathlib import Path from os.path import expanduser from pprint import pformat @@ -9,6 +8,8 @@ from psutil import Process, TimeoutExpired import colorama import six from . import logs, conf, types, shells +from .corrector import get_corrected_commands +from .ui import select_command def setup_user_dir(): @@ -21,37 +22,6 @@ def setup_user_dir(): return user_dir -def load_rule(rule): - """Imports rule module and returns it.""" - rule_module = load_source(rule.name[:-3], str(rule)) - return types.Rule(rule.name[:-3], rule_module.match, - rule_module.get_new_command, - getattr(rule_module, 'enabled_by_default', True), - getattr(rule_module, 'side_effect', None), - getattr(rule_module, 'priority', conf.DEFAULT_PRIORITY), - getattr(rule_module, 'requires_output', True)) - - -def _get_loaded_rules(rules, settings): - """Yields all available rules.""" - for rule in rules: - if rule.name != '__init__.py': - loaded_rule = load_rule(rule) - if loaded_rule in settings.rules: - yield loaded_rule - - -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') - rules = _get_loaded_rules(sorted(bundled) + sorted(user), settings) - return sorted(rules, key=lambda rule: settings.priority.get( - rule.name, rule.priority)) - - def wait_output(settings, popen): """Returns `True` if we can get output of the command in the `wait_command` time. @@ -100,46 +70,12 @@ def get_command(settings, args): return types.Command(script, None, None) -def get_matched_rule(command, rules, settings): - """Returns first matched rule for command.""" - script_only = command.stdout is None and command.stderr is None - - for rule in rules: - if script_only and rule.requires_output: - continue - - try: - with logs.debug_time(u'Trying rule: {};'.format(rule.name), - settings): - if rule.match(command, settings): - return rule - except Exception: - logs.rule_failed(rule, sys.exc_info(), settings) - - -def confirm(new_command, side_effect, settings): - """Returns `True` when running of new command confirmed.""" - if not settings.require_confirmation: - logs.show_command(new_command, side_effect, settings) - return True - - logs.confirm_command(new_command, side_effect, settings) - try: - sys.stdin.read(1) - return True - except KeyboardInterrupt: - logs.failed('Aborted', settings) - return False - - -def run_rule(rule, command, settings): +def run_command(command, settings): """Runs command from rule for passed command.""" - new_command = shells.to_shell(rule.get_new_command(command, settings)) - if confirm(new_command, rule.side_effect, settings): - if rule.side_effect: - rule.side_effect(command, settings) - shells.put_to_history(new_command) - print(new_command) + if command.side_effect: + command.side_effect(command, settings) + shells.put_to_history(command.script) + print(command.script) # Entry points: @@ -152,18 +88,10 @@ def main(): logs.debug(u'Run with settings: {}'.format(pformat(settings)), settings) command = get_command(settings, sys.argv) - rules = get_rules(user_dir, settings) - logs.debug( - u'Loaded rules: {}'.format(', '.join(rule.name for rule in rules)), - settings) - - matched_rule = get_matched_rule(command, rules, settings) - if matched_rule: - logs.debug(u'Matched rule: {}'.format(matched_rule.name), settings) - run_rule(matched_rule, command, settings) - return - - logs.failed('No fuck given', settings) + corrected_commands = get_corrected_commands(command, user_dir, settings) + selected_command = select_command(corrected_commands, settings) + if selected_command: + run_command(selected_command, settings) def print_alias(): diff --git a/thefuck/types.py b/thefuck/types.py index 71828edb..cc481381 100644 --- a/thefuck/types.py +++ b/thefuck/types.py @@ -3,6 +3,8 @@ from collections import namedtuple 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')) diff --git a/thefuck/ui.py b/thefuck/ui.py new file mode 100644 index 00000000..3e18e62f --- /dev/null +++ b/thefuck/ui.py @@ -0,0 +1,88 @@ +import sys +from getch import getch +from . import logs + +SELECT = 0 +ABORT = 1 +PREVIOUS = 2 +NEXT = 3 + + +def read_actions(): + """Yields actions for pressed keys.""" + buffer = [] + ch = None + while True: + try: + try: + ch = getch() + except OverflowError: # Ctrl+C, KeyboardInterrupt will be reraised + pass + except KeyboardInterrupt: + yield ABORT + + if ch in ('\n', '\r'): # Enter + yield SELECT + + buffer.append(ch) + buffer = buffer[-3:] + + if buffer == ['\x1b', '[', 'A']: # ↑ + yield PREVIOUS + + if buffer == ['\x1b', '[', 'B']: # ↓ + yield NEXT + + +class CommandSelector(object): + def __init__(self, commands): + self._commands = commands + self._index = 0 + self._on_change = lambda x: x + + def next(self): + self._index = (self._index + 1) % len(self._commands) + self._on_change(self.value) + + def previous(self): + self._index = (self._index - 1) % len(self._commands) + self._on_change(self.value) + + @property + def value(self): + return self._commands[self._index] + + def on_change(self, fn): + self._on_change = fn + fn(self.value) + + +def select_command(corrected_commands, settings): + """Returns: + + - the first command when confirmation disabled; + - None when ctrl+c pressed; + - selected command. + + """ + if not corrected_commands: + logs.failed('No fuck given', settings) + return + + selector = CommandSelector(corrected_commands) + if not settings.require_confirmation: + logs.show_corrected_command(selector.value, settings) + return selector.value + + selector.on_change(lambda val: logs.confirm_text(val, settings)) + for key in read_actions(): + if key == SELECT: + sys.stderr.write('\n') + return selector.value + elif key == ABORT: + logs.failed('\nAborted', settings) + return + elif key == PREVIOUS: + selector.previous() + elif key == NEXT: + selector.next() From c8550a0ce5f99f575b7f7dc4be9be77604f86e58 Mon Sep 17 00:00:00 2001 From: nvbn Date: Wed, 29 Jul 2015 15:22:24 +0300 Subject: [PATCH 02/14] #298 Fix python 2 support --- setup.py | 2 +- tests/test_ui.py | 10 ++++++---- thefuck/logs.py | 2 ++ thefuck/ui.py | 28 +++++++++++++++++++++------- 4 files changed, 30 insertions(+), 12 deletions(-) diff --git a/setup.py b/setup.py index dadcb36a..bdfcbf68 100755 --- a/setup.py +++ b/setup.py @@ -22,7 +22,7 @@ elif (3, 0) < version < (3, 3): VERSION = '2.5.6' -install_requires = ['psutil', 'colorama', 'six', 'getch'] +install_requires = ['psutil', 'colorama', 'six'] extras_require = {':python_version<"3.4"': ['pathlib']} setup(name='thefuck', diff --git a/tests/test_ui.py b/tests/test_ui.py index 3d7d0186..578abce2 100644 --- a/tests/test_ui.py +++ b/tests/test_ui.py @@ -1,3 +1,5 @@ +# -*- encoding: utf-8 -*- + from mock import Mock import pytest from itertools import islice @@ -88,14 +90,14 @@ class TestSelectCommand(object): assert ui.select_command(commands, Mock(debug=False, no_color=True, require_confirmation=True)) == commands[0] - assert capsys.readouterr() == ('', '\x1b[1K\rls [enter/↑/↓/ctrl+c]\n') + 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, Mock(debug=False, no_color=True, require_confirmation=True)) is None - assert capsys.readouterr() == ('', '\x1b[1K\rls [enter/↑/↓/ctrl+c]\nAborted\n') + assert capsys.readouterr() == ('', u'\x1b[1K\rls [enter/↑/↓/ctrl+c]\nAborted\n') def test_with_confirmation_with_side_effct(self, capsys, patch_getch, commands_with_side_effect): @@ -104,7 +106,7 @@ class TestSelectCommand(object): Mock(debug=False, no_color=True, require_confirmation=True))\ == commands_with_side_effect[0] - assert capsys.readouterr() == ('', '\x1b[1K\rls (+side effect) [enter/↑/↓/ctrl+c]\n') + assert capsys.readouterr() == ('', u'\x1b[1K\rls (+side effect) [enter/↑/↓/ctrl+c]\n') def test_with_confirmation_select_second(self, capsys, patch_getch, commands): patch_getch(['\x1b', '[', 'B', '\n']) @@ -112,4 +114,4 @@ class TestSelectCommand(object): Mock(debug=False, no_color=True, require_confirmation=True)) == commands[1] assert capsys.readouterr() == ( - '', '\x1b[1K\rls [enter/↑/↓/ctrl+c]\x1b[1K\rcd [enter/↑/↓/ctrl+c]\n') + '', u'\x1b[1K\rls [enter/↑/↓/ctrl+c]\x1b[1K\rcd [enter/↑/↓/ctrl+c]\n') diff --git a/thefuck/logs.py b/thefuck/logs.py index fcdbeca2..08781024 100644 --- a/thefuck/logs.py +++ b/thefuck/logs.py @@ -1,3 +1,5 @@ +# -*- encoding: utf-8 -*- + from contextlib import contextmanager from datetime import datetime import sys diff --git a/thefuck/ui.py b/thefuck/ui.py index 3e18e62f..22243d39 100644 --- a/thefuck/ui.py +++ b/thefuck/ui.py @@ -1,7 +1,25 @@ +# -*- encoding: utf-8 -*- + import sys -from getch import getch from . import logs +try: + from msvcrt import getch +except ImportError: + def getch(): + import tty + import termios + + fd = sys.stdin.fileno() + old = termios.tcgetattr(fd) + try: + tty.setraw(fd) + ch = sys.stdin.read(1) + if ch == '\x03': # For compatibility with msvcrt.getch + raise KeyboardInterrupt + finally: + termios.tcsetattr(fd, termios.TCSADRAIN, old) + SELECT = 0 ABORT = 1 PREVIOUS = 2 @@ -11,14 +29,10 @@ NEXT = 3 def read_actions(): """Yields actions for pressed keys.""" buffer = [] - ch = None while True: try: - try: - ch = getch() - except OverflowError: # Ctrl+C, KeyboardInterrupt will be reraised - pass - except KeyboardInterrupt: + ch = getch() + except KeyboardInterrupt: # Ctrl+C yield ABORT if ch in ('\n', '\r'): # Enter From e6af00ef976b5ff2953083eb10a23fbc4ba7f31d Mon Sep 17 00:00:00 2001 From: nvbn Date: Wed, 29 Jul 2015 15:33:29 +0300 Subject: [PATCH 03/14] #298 Fix selecting command --- thefuck/ui.py | 1 + 1 file changed, 1 insertion(+) diff --git a/thefuck/ui.py b/thefuck/ui.py index 22243d39..257b6e66 100644 --- a/thefuck/ui.py +++ b/thefuck/ui.py @@ -17,6 +17,7 @@ except ImportError: ch = sys.stdin.read(1) if ch == '\x03': # For compatibility with msvcrt.getch raise KeyboardInterrupt + return ch finally: termios.tcsetattr(fd, termios.TCSADRAIN, old) From 4bc1cc78496e8b7807ab3ffdda2b035e44c4c33e Mon Sep 17 00:00:00 2001 From: nvbn Date: Wed, 29 Jul 2015 15:40:21 +0300 Subject: [PATCH 04/14] #298 Add support of list results in `sudo_support` --- tests/test_utils.py | 1 + thefuck/utils.py | 10 ++++++++++ 2 files changed, 11 insertions(+) diff --git a/tests/test_utils.py b/tests/test_utils.py index e47db872..31b013ec 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -18,6 +18,7 @@ def test_wrap_settings(override, old, new): @pytest.mark.parametrize('return_value, command, called, result', [ ('ls -lah', 'sudo ls', 'ls', 'sudo ls -lah'), ('ls -lah', 'ls', 'ls', 'ls -lah'), + (['ls -lah'], 'sudo ls', 'ls', ['sudo ls -lah']), (True, 'sudo ls', 'ls', True), (True, 'ls', 'ls', True), (False, 'sudo ls', 'ls', False), diff --git a/thefuck/utils.py b/thefuck/utils.py index 639fa4d8..db579fe9 100644 --- a/thefuck/utils.py +++ b/thefuck/utils.py @@ -69,6 +69,8 @@ def sudo_support(fn): 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 @@ -161,6 +163,14 @@ 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 + + +@eager def get_all_matched_commands(stderr, separator='Did you mean'): should_yield = False for line in stderr.split('\n'): From d6e80b78353701bba834ede58cc641fd21abe432 Mon Sep 17 00:00:00 2001 From: nvbn Date: Wed, 29 Jul 2015 16:09:26 +0300 Subject: [PATCH 05/14] #298 Suggest more than one result in *_no_command rules --- tests/rules/test_brew_unknown_command.py | 4 ++-- tests/rules/test_docker_not_command.py | 6 +++--- tests/rules/test_git_not_command.py | 6 +++--- tests/rules/test_gulp_not_task.py | 2 +- tests/rules/test_heroku_not_command.py | 4 ++-- tests/rules/test_lein_not_task.py | 3 ++- tests/rules/test_no_command.py | 4 ++-- tests/rules/test_tsuru_not_command.py | 14 +++++++------- thefuck/rules/brew_unknown_command.py | 12 +++--------- thefuck/rules/docker_not_command.py | 5 ++--- thefuck/rules/git_not_command.py | 10 ++++------ thefuck/rules/gulp_not_task.py | 5 ++--- thefuck/rules/heroku_not_command.py | 5 ++--- thefuck/rules/lein_not_task.py | 8 ++++---- thefuck/rules/no_command.py | 7 ++++--- thefuck/rules/tsuru_not_command.py | 7 +++---- thefuck/utils.py | 7 +++++++ 17 files changed, 53 insertions(+), 56 deletions(-) diff --git a/tests/rules/test_brew_unknown_command.py b/tests/rules/test_brew_unknown_command.py index d9a79c30..b33f7409 100644 --- a/tests/rules/test_brew_unknown_command.py +++ b/tests/rules/test_brew_unknown_command.py @@ -22,7 +22,7 @@ def test_match(brew_unknown_cmd): def test_get_new_command(brew_unknown_cmd, brew_unknown_cmd2): assert get_new_command(Command('brew inst', stderr=brew_unknown_cmd), - None) == 'brew list' + None) == ['brew list', 'brew install', 'brew uninstall'] assert get_new_command(Command('brew instaa', stderr=brew_unknown_cmd2), - None) == 'brew install' + None) == ['brew install', 'brew uninstall', 'brew list'] diff --git a/tests/rules/test_docker_not_command.py b/tests/rules/test_docker_not_command.py index 1e7b0061..5e9a7811 100644 --- a/tests/rules/test_docker_not_command.py +++ b/tests/rules/test_docker_not_command.py @@ -122,8 +122,8 @@ def test_not_match(script, stderr): @pytest.mark.usefixtures('docker_help') @pytest.mark.parametrize('wrong, fixed', [ - ('pes', 'ps'), - ('tags', 'tag')]) + ('pes', ['ps', 'push', 'pause']), + ('tags', ['tag', 'stats', 'images'])]) def test_get_new_command(wrong, fixed): command = Command('docker {}'.format(wrong), stderr=stderr(wrong)) - assert get_new_command(command, None) == 'docker {}'.format(fixed) + assert get_new_command(command, None) == ['docker {}'.format(x) for x in fixed] diff --git a/tests/rules/test_git_not_command.py b/tests/rules/test_git_not_command.py index 76f5817b..0bff9803 100644 --- a/tests/rules/test_git_not_command.py +++ b/tests/rules/test_git_not_command.py @@ -50,8 +50,8 @@ def test_match(git_not_command, git_command, git_not_command_one_of_this): def test_get_new_command(git_not_command, git_not_command_one_of_this, git_not_command_closest): assert get_new_command(Command('git brnch', stderr=git_not_command), None) \ - == 'git branch' + == ['git branch'] assert get_new_command(Command('git st', stderr=git_not_command_one_of_this), - None) == 'git status' + None) == ['git stats', 'git stash', 'git stage'] assert get_new_command(Command('git tags', stderr=git_not_command_closest), - None) == 'git tag' + None) == ['git tag', 'git stage'] diff --git a/tests/rules/test_gulp_not_task.py b/tests/rules/test_gulp_not_task.py index f5fa09b0..980e0f73 100644 --- a/tests/rules/test_gulp_not_task.py +++ b/tests/rules/test_gulp_not_task.py @@ -25,4 +25,4 @@ def test_get_new_command(mocker): mocker.patch('thefuck.rules.gulp_not_task.get_gulp_tasks', return_value=[ 'serve', 'build', 'default']) command = Command('gulp srve', stdout('srve')) - assert get_new_command(command, None) == 'gulp serve' + assert get_new_command(command, None) == ['gulp serve', 'gulp default'] diff --git a/tests/rules/test_heroku_not_command.py b/tests/rules/test_heroku_not_command.py index acbda7ae..c1f5a761 100644 --- a/tests/rules/test_heroku_not_command.py +++ b/tests/rules/test_heroku_not_command.py @@ -27,8 +27,8 @@ def test_not_match(script, stderr): @pytest.mark.parametrize('cmd, result', [ - ('log', 'heroku logs'), - ('pge', 'heroku pg')]) + ('log', ['heroku logs', 'heroku pg']), + ('pge', ['heroku pg', 'heroku logs'])]) def test_get_new_command(cmd, result): command = Command('heroku {}'.format(cmd), stderr=suggest_stderr(cmd)) assert get_new_command(command, None) == result diff --git a/tests/rules/test_lein_not_task.py b/tests/rules/test_lein_not_task.py index 35975b82..9eef9b44 100644 --- a/tests/rules/test_lein_not_task.py +++ b/tests/rules/test_lein_not_task.py @@ -9,6 +9,7 @@ def is_not_task(): Did you mean this? repl + jar ''' @@ -19,4 +20,4 @@ def test_match(is_not_task): def test_get_new_command(is_not_task): assert get_new_command(Mock(script='lein rpl --help', stderr=is_not_task), - None) == 'lein repl --help' + None) == ['lein repl --help', 'lein jar --help'] diff --git a/tests/rules/test_no_command.py b/tests/rules/test_no_command.py index 6159d76b..a44c1fd9 100644 --- a/tests/rules/test_no_command.py +++ b/tests/rules/test_no_command.py @@ -22,8 +22,8 @@ def test_get_new_command(): assert get_new_command( Command(stderr='vom: not found', script='vom file.py'), - None) == 'vim file.py' + None) == ['vim file.py'] assert get_new_command( Command(stderr='fucck: not found', script='fucck'), - Command) == 'fsck' + Command) == ['fsck'] diff --git a/tests/rules/test_tsuru_not_command.py b/tests/rules/test_tsuru_not_command.py index d8d8c99a..80baf7e4 100644 --- a/tests/rules/test_tsuru_not_command.py +++ b/tests/rules/test_tsuru_not_command.py @@ -61,30 +61,30 @@ def test_not_match(command): assert not match(command, None) -@pytest.mark.parametrize('command, new_command', [ +@pytest.mark.parametrize('command, new_commands', [ (Command('tsuru log', stderr=( 'tsuru: "log" is not a tsuru command. See "tsuru help".\n' '\nDid you mean?\n' '\tapp-log\n' '\tlogin\n' '\tlogout\n' - )), 'tsuru login'), + )), ['tsuru login', 'tsuru logout', 'tsuru app-log']), (Command('tsuru app-l', stderr=( 'tsuru: "app-l" is not a tsuru command. See "tsuru help".\n' '\nDid you mean?\n' '\tapp-list\n' '\tapp-log\n' - )), 'tsuru app-log'), + )), ['tsuru app-log', 'tsuru app-list']), (Command('tsuru user-list', stderr=( 'tsuru: "user-list" is not a tsuru command. See "tsuru help".\n' '\nDid you mean?\n' '\tteam-user-list\n' - )), 'tsuru team-user-list'), + )), ['tsuru team-user-list']), (Command('tsuru targetlist', stderr=( 'tsuru: "targetlist" is not a tsuru command. See "tsuru help".\n' '\nDid you mean?\n' '\ttarget-list\n' - )), 'tsuru target-list'), + )), ['tsuru target-list']), ]) -def test_get_new_command(command, new_command): - assert get_new_command(command, None) == new_command +def test_get_new_command(command, new_commands): + assert get_new_command(command, None) == new_commands diff --git a/thefuck/rules/brew_unknown_command.py b/thefuck/rules/brew_unknown_command.py index 0a2402f3..9b80f887 100644 --- a/thefuck/rules/brew_unknown_command.py +++ b/thefuck/rules/brew_unknown_command.py @@ -1,7 +1,7 @@ import os import re import subprocess -from thefuck.utils import get_closest, replace_argument +from thefuck.utils import get_closest, replace_command BREW_CMD_PATH = '/Library/Homebrew/cmd' TAP_PATH = '/Library/Taps' @@ -77,10 +77,6 @@ if brew_path_prefix: pass -def _get_similar_command(command): - return get_closest(command, brew_commands) - - def match(command, settings): is_proper_command = ('brew' in command.script and 'Unknown command' in command.stderr) @@ -89,7 +85,7 @@ def match(command, settings): if is_proper_command: broken_cmd = re.findall(r'Error: Unknown command: ([a-z]+)', command.stderr)[0] - has_possible_commands = bool(_get_similar_command(broken_cmd)) + has_possible_commands = bool(get_closest(broken_cmd, brew_commands)) return has_possible_commands @@ -97,6 +93,4 @@ def match(command, settings): def get_new_command(command, settings): broken_cmd = re.findall(r'Error: Unknown command: ([a-z]+)', command.stderr)[0] - new_cmd = _get_similar_command(broken_cmd) - - return replace_argument(command.script, broken_cmd, new_cmd) + return replace_command(command, broken_cmd, brew_commands) diff --git a/thefuck/rules/docker_not_command.py b/thefuck/rules/docker_not_command.py index 4b473bbf..d866ee27 100644 --- a/thefuck/rules/docker_not_command.py +++ b/thefuck/rules/docker_not_command.py @@ -1,7 +1,7 @@ from itertools import dropwhile, takewhile, islice import re import subprocess -from thefuck.utils import get_closest, sudo_support, replace_argument +from thefuck.utils import get_closest, sudo_support, replace_argument, replace_command @sudo_support @@ -23,5 +23,4 @@ def get_docker_commands(): def get_new_command(command, settings): wrong_command = re.findall( r"docker: '(\w+)' is not a docker command.", command.stderr)[0] - fixed_command = get_closest(wrong_command, get_docker_commands()) - return replace_argument(command.script, wrong_command, fixed_command) + return replace_command(command, wrong_command, get_docker_commands()) diff --git a/thefuck/rules/git_not_command.py b/thefuck/rules/git_not_command.py index 033e0095..65df0a97 100644 --- a/thefuck/rules/git_not_command.py +++ b/thefuck/rules/git_not_command.py @@ -1,6 +1,6 @@ import re -from thefuck.utils import (get_closest, git_support, replace_argument, - get_all_matched_commands) +from thefuck.utils import (git_support, + get_all_matched_commands, replace_command) @git_support @@ -13,7 +13,5 @@ def match(command, settings): def get_new_command(command, settings): broken_cmd = re.findall(r"git: '([^']*)' is not a git command", command.stderr)[0] - new_cmd = get_closest(broken_cmd, - get_all_matched_commands(command.stderr)) - return replace_argument(command.script, broken_cmd, new_cmd) - + matched = get_all_matched_commands(command.stderr) + return replace_command(command, broken_cmd, matched) diff --git a/thefuck/rules/gulp_not_task.py b/thefuck/rules/gulp_not_task.py index c7ecc99b..c1a548c1 100644 --- a/thefuck/rules/gulp_not_task.py +++ b/thefuck/rules/gulp_not_task.py @@ -1,6 +1,6 @@ import re import subprocess -from thefuck.utils import get_closest, replace_argument +from thefuck.utils import replace_command def match(command, script): @@ -18,5 +18,4 @@ def get_gulp_tasks(): def get_new_command(command, script): wrong_task = re.findall(r"Task '(\w+)' is not in your gulpfile", command.stdout)[0] - fixed_task = get_closest(wrong_task, get_gulp_tasks()) - return replace_argument(command.script, wrong_task, fixed_task) + return replace_command(command, wrong_task, get_gulp_tasks()) diff --git a/thefuck/rules/heroku_not_command.py b/thefuck/rules/heroku_not_command.py index 04600b31..87360bc8 100644 --- a/thefuck/rules/heroku_not_command.py +++ b/thefuck/rules/heroku_not_command.py @@ -1,5 +1,5 @@ import re -from thefuck.utils import get_closest, replace_argument +from thefuck.utils import replace_command def match(command, settings): @@ -16,5 +16,4 @@ def _get_suggests(stderr): def get_new_command(command, settings): wrong = re.findall(r'`(\w+)` is not a heroku command', command.stderr)[0] - correct = get_closest(wrong, _get_suggests(command.stderr)) - return replace_argument(command.script, wrong, correct) + return replace_command(command, wrong, _get_suggests(command.stderr)) diff --git a/thefuck/rules/lein_not_task.py b/thefuck/rules/lein_not_task.py index 6ecdd38b..34e46fe3 100644 --- a/thefuck/rules/lein_not_task.py +++ b/thefuck/rules/lein_not_task.py @@ -1,5 +1,6 @@ import re -from thefuck.utils import sudo_support, replace_argument +from thefuck.utils import sudo_support,\ + replace_command, get_all_matched_commands @sudo_support @@ -13,6 +14,5 @@ def match(command, settings): def get_new_command(command, settings): broken_cmd = re.findall(r"'([^']*)' is not a task", command.stderr)[0] - new_cmd = re.findall(r'Did you mean this\?\n\s*([^\n]*)', - command.stderr)[0] - return replace_argument(command.script, broken_cmd, new_cmd) + new_cmds = get_all_matched_commands(command.stderr, 'Did you mean this?') + return replace_command(command, broken_cmd, new_cmds) diff --git a/thefuck/rules/no_command.py b/thefuck/rules/no_command.py index f414ba3b..a8258ce0 100644 --- a/thefuck/rules/no_command.py +++ b/thefuck/rules/no_command.py @@ -1,5 +1,5 @@ from difflib import get_close_matches -from thefuck.utils import sudo_support, get_all_executables, get_closest +from thefuck.utils import sudo_support, get_all_executables @sudo_support @@ -12,8 +12,9 @@ def match(command, settings): @sudo_support def get_new_command(command, settings): old_command = command.script.split(' ')[0] - new_command = get_closest(old_command, get_all_executables()) - return ' '.join([new_command] + command.script.split(' ')[1:]) + new_cmds = get_close_matches(old_command, get_all_executables(), cutoff=0.1) + return [' '.join([new_command] + command.script.split(' ')[1:]) + for new_command in new_cmds] priority = 3000 diff --git a/thefuck/rules/tsuru_not_command.py b/thefuck/rules/tsuru_not_command.py index 29930a5a..1115a3ae 100644 --- a/thefuck/rules/tsuru_not_command.py +++ b/thefuck/rules/tsuru_not_command.py @@ -1,6 +1,6 @@ import re from thefuck.utils import (get_closest, replace_argument, - get_all_matched_commands) + get_all_matched_commands, replace_command) def match(command, settings): @@ -12,7 +12,6 @@ def match(command, settings): def get_new_command(command, settings): broken_cmd = re.findall(r'tsuru: "([^"]*)" is not a tsuru command', command.stderr)[0] - new_cmd = get_closest(broken_cmd, - get_all_matched_commands(command.stderr)) - return replace_argument(command.script, broken_cmd, new_cmd) + return replace_command(command, broken_cmd, + get_all_matched_commands(command.stderr)) diff --git a/thefuck/utils.py b/thefuck/utils.py index db579fe9..dde7c906 100644 --- a/thefuck/utils.py +++ b/thefuck/utils.py @@ -178,3 +178,10 @@ def get_all_matched_commands(stderr, separator='Did you mean'): should_yield = True elif should_yield and line: yield line.strip() + + +def replace_command(command, broken, matched): + """Helper for *_no_command rules.""" + 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] From 8962cf2ec1f3e12892f7a4891b776e6a55b87674 Mon Sep 17 00:00:00 2001 From: nvbn Date: Wed, 29 Jul 2015 16:11:23 +0300 Subject: [PATCH 06/14] #298 Use `eager` decorator when we don't need lazines --- thefuck/corrector.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/thefuck/corrector.py b/thefuck/corrector.py index d9bef851..d1b4cfc8 100644 --- a/thefuck/corrector.py +++ b/thefuck/corrector.py @@ -1,7 +1,8 @@ +import sys from imp import load_source from pathlib import Path from . import conf, types, logs -import sys +from .utils import eager def load_rule(rule, settings): @@ -26,6 +27,7 @@ def get_loaded_rules(rules, settings): yield loaded_rule +@eager def get_rules(user_dir, settings): """Returns all enabled rules.""" bundled = Path(__file__).parent \ @@ -35,6 +37,7 @@ def get_rules(user_dir, settings): return get_loaded_rules(sorted(bundled) + sorted(user), settings) +@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 @@ -64,11 +67,11 @@ def make_corrected_commands(command, rules, settings): def get_corrected_commands(command, user_dir, settings): - rules = list(get_rules(user_dir, settings)) + rules = get_rules(user_dir, settings) logs.debug( u'Loaded rules: {}'.format(', '.join(rule.name for rule in rules)), settings) - matched = list(get_matched_rules(command, rules, settings)) + matched = get_matched_rules(command, rules, settings) logs.debug( u'Matched rules: {}'.format(', '.join(rule.name for rule in matched)), settings) From 9d91b967809d6056cde1ae64d8bebcf4d72a2551 Mon Sep 17 00:00:00 2001 From: nvbn Date: Wed, 29 Jul 2015 16:30:32 +0300 Subject: [PATCH 07/14] #298 Simplify func tests --- tests/functional/plots.py | 20 +++++++++------- tests/functional/test_bash.py | 45 ++++++++++++++++------------------- tests/functional/test_fish.py | 39 ++++++++++++++---------------- tests/functional/test_tcsh.py | 35 +++++++++++++-------------- tests/functional/test_zsh.py | 45 ++++++++++++++++------------------- tests/functional/utils.py | 9 +++---- 6 files changed, 89 insertions(+), 104 deletions(-) diff --git a/tests/functional/plots.py b/tests/functional/plots.py index 3a007051..46eaf005 100644 --- a/tests/functional/plots.py +++ b/tests/functional/plots.py @@ -1,10 +1,16 @@ from pexpect import TIMEOUT +def _set_confirmation(proc, require): + proc.sendline(u'mkdir -p ~/.thefuck') + proc.sendline( + u'echo "require_confirmation = {}" > ~/.thefuck/settings.py'.format( + require)) + + def with_confirmation(proc): """Ensures that command can be fixed when confirmation enabled.""" - proc.sendline(u'mkdir -p ~/.thefuck') - proc.sendline(u'echo "require_confirmation = True" > ~/.thefuck/settings.py') + _set_confirmation(proc, True) proc.sendline(u'ehco test') @@ -17,10 +23,10 @@ def with_confirmation(proc): assert proc.expect([TIMEOUT, u'test']) -def history_changed(proc): +def history_changed(proc, to=u'echo test'): """Ensures that history changed.""" proc.send('\033[A') - assert proc.expect([TIMEOUT, u'echo test']) + assert proc.expect([TIMEOUT, to]) def history_not_changed(proc): @@ -31,8 +37,7 @@ def history_not_changed(proc): def refuse_with_confirmation(proc): """Ensures that fix can be refused when confirmation enabled.""" - proc.sendline(u'mkdir -p ~/.thefuck') - proc.sendline(u'echo "require_confirmation = True" > ~/.thefuck/settings.py') + _set_confirmation(proc, True) proc.sendline(u'ehco test') @@ -47,8 +52,7 @@ def refuse_with_confirmation(proc): def without_confirmation(proc): """Ensures that command can be fixed when confirmation disabled.""" - proc.sendline(u'mkdir -p ~/.thefuck') - proc.sendline(u'echo "require_confirmation = False" > ~/.thefuck/settings.py') + _set_confirmation(proc, False) proc.sendline(u'ehco test') diff --git a/tests/functional/test_bash.py b/tests/functional/test_bash.py index d36e9bb9..a40361cd 100644 --- a/tests/functional/test_bash.py +++ b/tests/functional/test_bash.py @@ -18,34 +18,29 @@ RUN pip2 install -U pip setuptools ''')) -@functional -@pytest.mark.parametrize('tag, dockerfile', containers) -def test_with_confirmation(tag, dockerfile): - with spawn(tag, dockerfile, u'bash') as proc: - proc.sendline(u"export PS1='$ '") - proc.sendline(u'eval $(thefuck-alias)') - proc.sendline(u'touch $HISTFILE') - with_confirmation(proc) - history_changed(proc) +@pytest.fixture(params=containers) +def proc(request): + tag, dockerfile = request.param + proc = spawn(request, tag, dockerfile, u'bash') + proc.sendline(u"export PS1='$ '") + proc.sendline(u'eval $(thefuck-alias)') + proc.sendline(u'touch $HISTFILE') + return proc @functional -@pytest.mark.parametrize('tag, dockerfile', containers) -def test_refuse_with_confirmation(tag, dockerfile): - with spawn(tag, dockerfile, u'bash') as proc: - proc.sendline(u"export PS1='$ '") - proc.sendline(u'eval $(thefuck-alias)') - proc.sendline(u'touch $HISTFILE') - refuse_with_confirmation(proc) - history_not_changed(proc) +def test_with_confirmation(proc): + with_confirmation(proc) + history_changed(proc) @functional -@pytest.mark.parametrize('tag, dockerfile', containers) -def test_without_confirmation(tag, dockerfile): - with spawn(tag, dockerfile, u'bash') as proc: - proc.sendline(u"export PS1='$ '") - proc.sendline(u'eval $(thefuck-alias)') - proc.sendline(u'touch $HISTFILE') - without_confirmation(proc) - history_changed(proc) +def test_refuse_with_confirmation(proc): + refuse_with_confirmation(proc) + history_not_changed(proc) + + +@functional +def test_without_confirmation(proc): + without_confirmation(proc) + history_changed(proc) diff --git a/tests/functional/test_fish.py b/tests/functional/test_fish.py index 89ad11ac..d465faf3 100644 --- a/tests/functional/test_fish.py +++ b/tests/functional/test_fish.py @@ -18,36 +18,33 @@ RUN pip2 install -U pip setuptools ''')) -@functional -@pytest.mark.skipif( - bool(bare), reason='https://github.com/travis-ci/apt-source-whitelist/issues/71') -@pytest.mark.parametrize('tag, dockerfile', containers) -def test_with_confirmation(tag, dockerfile): - with spawn(tag, dockerfile, u'fish') as proc: - proc.sendline(u'thefuck-alias > ~/.config/fish/config.fish') - proc.sendline(u'fish') - with_confirmation(proc) +@pytest.fixture(params=containers) +def proc(request): + tag, dockerfile = request.param + proc = spawn(request, tag, dockerfile, u'fish') + proc.sendline(u'thefuck-alias > ~/.config/fish/config.fish') + proc.sendline(u'fish') + return proc @functional @pytest.mark.skipif( bool(bare), reason='https://github.com/travis-ci/apt-source-whitelist/issues/71') -@pytest.mark.parametrize('tag, dockerfile', containers) -def test_refuse_with_confirmation(tag, dockerfile): - with spawn(tag, dockerfile, u'fish') as proc: - proc.sendline(u'thefuck-alias > ~/.config/fish/config.fish') - proc.sendline(u'fish') - refuse_with_confirmation(proc) +def test_with_confirmation(proc): + with_confirmation(proc) @functional @pytest.mark.skipif( bool(bare), reason='https://github.com/travis-ci/apt-source-whitelist/issues/71') -@pytest.mark.parametrize('tag, dockerfile', containers) -def test_without_confirmation(tag, dockerfile): - with spawn(tag, dockerfile, u'fish') as proc: - proc.sendline(u'thefuck-alias > ~/.config/fish/config.fish') - proc.sendline(u'fish') - without_confirmation(proc) +def test_refuse_with_confirmation(proc): + refuse_with_confirmation(proc) + + +@functional +@pytest.mark.skipif( + bool(bare), reason='https://github.com/travis-ci/apt-source-whitelist/issues/71') +def test_without_confirmation(proc): + without_confirmation(proc) # TODO: ensure that history changes. diff --git a/tests/functional/test_tcsh.py b/tests/functional/test_tcsh.py index 674c106b..6dd926cc 100644 --- a/tests/functional/test_tcsh.py +++ b/tests/functional/test_tcsh.py @@ -18,30 +18,27 @@ RUN pip2 install -U pip setuptools ''')) -@functional -@pytest.mark.parametrize('tag, dockerfile', containers) -def test_with_confirmation(tag, dockerfile): - with spawn(tag, dockerfile, u'tcsh') as proc: - proc.sendline(u'tcsh') - proc.sendline(u'eval `thefuck-alias`') - with_confirmation(proc) +@pytest.fixture(params=containers) +def proc(request): + tag, dockerfile = request.param + proc = spawn(request, tag, dockerfile, u'tcsh') + proc.sendline(u'tcsh') + proc.sendline(u'eval `thefuck-alias`') + return proc @functional -@pytest.mark.parametrize('tag, dockerfile', containers) -def test_refuse_with_confirmation(tag, dockerfile): - with spawn(tag, dockerfile, u'tcsh') as proc: - proc.sendline(u'tcsh') - proc.sendline(u'eval `thefuck-alias`') - refuse_with_confirmation(proc) +def test_with_confirmation(proc): + with_confirmation(proc) @functional -@pytest.mark.parametrize('tag, dockerfile', containers) -def test_without_confirmation(tag, dockerfile): - with spawn(tag, dockerfile, u'tcsh') as proc: - proc.sendline(u'tcsh') - proc.sendline(u'eval `thefuck-alias`') - without_confirmation(proc) +def test_refuse_with_confirmation(proc): + refuse_with_confirmation(proc) + + +@functional +def test_without_confirmation(proc): + without_confirmation(proc) # TODO: ensure that history changes. diff --git a/tests/functional/test_zsh.py b/tests/functional/test_zsh.py index da46d94a..0201fa36 100644 --- a/tests/functional/test_zsh.py +++ b/tests/functional/test_zsh.py @@ -18,34 +18,29 @@ RUN pip2 install -U pip setuptools ''')) -@functional -@pytest.mark.parametrize('tag, dockerfile', containers) -def test_with_confirmation(tag, dockerfile): - with spawn(tag, dockerfile, u'zsh') as proc: - proc.sendline(u'eval $(thefuck-alias)') - proc.sendline(u'export HISTFILE=~/.zsh_history') - proc.sendline(u'touch $HISTFILE') - with_confirmation(proc) - history_changed(proc) +@pytest.fixture(params=containers) +def proc(request): + tag, dockerfile = request.param + proc = spawn(request, tag, dockerfile, u'zsh') + proc.sendline(u'eval $(thefuck-alias)') + proc.sendline(u'export HISTFILE=~/.zsh_history') + proc.sendline(u'touch $HISTFILE') + return proc @functional -@pytest.mark.parametrize('tag, dockerfile', containers) -def test_refuse_with_confirmation(tag, dockerfile): - with spawn(tag, dockerfile, u'zsh') as proc: - proc.sendline(u'eval $(thefuck-alias)') - proc.sendline(u'export HISTFILE=~/.zsh_history') - proc.sendline(u'touch $HISTFILE') - refuse_with_confirmation(proc) - history_not_changed(proc) +def test_with_confirmation(proc): + with_confirmation(proc) + history_changed(proc) @functional -@pytest.mark.parametrize('tag, dockerfile', containers) -def test_without_confirmation(tag, dockerfile): - with spawn(tag, dockerfile, u'zsh') as proc: - proc.sendline(u'eval $(thefuck-alias)') - proc.sendline(u'export HISTFILE=~/.zsh_history') - proc.sendline(u'touch $HISTFILE') - without_confirmation(proc) - history_changed(proc) +def test_refuse_with_confirmation(proc): + refuse_with_confirmation(proc) + history_not_changed(proc) + + +@functional +def test_without_confirmation(proc): + without_confirmation(proc) + history_changed(proc) diff --git a/tests/functional/utils.py b/tests/functional/utils.py index 1650ab0b..69edb4b8 100644 --- a/tests/functional/utils.py +++ b/tests/functional/utils.py @@ -23,8 +23,7 @@ def build_container(tag, dockerfile): shutil.rmtree(tmpdir) -@contextmanager -def spawn(tag, dockerfile, cmd): +def spawn(request, tag, dockerfile, cmd): if bare: proc = pexpect.spawnu(cmd) else: @@ -36,10 +35,8 @@ def spawn(tag, dockerfile, cmd): proc.logfile = sys.stdout - try: - yield proc - finally: - proc.terminate(force=bare) + request.addfinalizer(proc.terminate) + return proc def images(*items): From 1a76bfd2a3f9222c39148b88b5534ce6a57fa280 Mon Sep 17 00:00:00 2001 From: nvbn Date: Thu, 30 Jul 2015 18:17:29 +0300 Subject: [PATCH 08/14] #298 Always clean-up after building container --- tests/functional/utils.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/tests/functional/utils.py b/tests/functional/utils.py index 69edb4b8..2ca2b839 100644 --- a/tests/functional/utils.py +++ b/tests/functional/utils.py @@ -1,5 +1,4 @@ import os -from contextlib import contextmanager import subprocess import shutil from tempfile import mkdtemp @@ -15,12 +14,14 @@ enabled = os.environ.get('FUNCTIONAL') def build_container(tag, dockerfile): tmpdir = mkdtemp() - with Path(tmpdir).joinpath('Dockerfile').open('w') as file: - file.write(dockerfile) - if subprocess.call(['docker', 'build', '--tag={}'.format(tag), tmpdir], - cwd=root) != 0: - raise Exception("Can't build a container") - shutil.rmtree(tmpdir) + try: + with Path(tmpdir).joinpath('Dockerfile').open('w') as file: + file.write(dockerfile) + if subprocess.call(['docker', 'build', '--tag={}'.format(tag), tmpdir], + cwd=root) != 0: + raise Exception("Can't build a container") + finally: + shutil.rmtree(tmpdir) def spawn(request, tag, dockerfile, cmd): @@ -32,6 +33,7 @@ def spawn(request, tag, dockerfile, cmd): proc = pexpect.spawnu('docker run --volume {}:/src --tty=true ' '--interactive=true {} {}'.format(root, tag, cmd)) proc.sendline('pip install /src') + proc.sendline('cd /') proc.logfile = sys.stdout From 70c89164b0c148d87ade21d6a25090e203e84272 Mon Sep 17 00:00:00 2001 From: nvbn Date: Thu, 30 Jul 2015 18:28:20 +0300 Subject: [PATCH 09/14] #298 Add func tests for selecting rule --- .travis.yml | 1 + tests/functional/plots.py | 22 +++++++++++++++++++++- tests/functional/test_bash.py | 17 ++++++++++++----- tests/functional/test_fish.py | 13 ++++++++++--- tests/functional/test_tcsh.py | 13 ++++++++++--- tests/functional/test_zsh.py | 18 +++++++++++++----- thefuck/ui.py | 10 +++++----- 7 files changed, 72 insertions(+), 22 deletions(-) diff --git a/.travis.yml b/.travis.yml index 1fea4fc7..65dc39fb 100644 --- a/.travis.yml +++ b/.travis.yml @@ -13,6 +13,7 @@ addons: - fish - tcsh - pandoc + - git env: - FUNCTIONAL=true BARE=true install: diff --git a/tests/functional/plots.py b/tests/functional/plots.py index 46eaf005..611742f2 100644 --- a/tests/functional/plots.py +++ b/tests/functional/plots.py @@ -23,7 +23,7 @@ def with_confirmation(proc): assert proc.expect([TIMEOUT, u'test']) -def history_changed(proc, to=u'echo test'): +def history_changed(proc, to): """Ensures that history changed.""" proc.send('\033[A') assert proc.expect([TIMEOUT, to]) @@ -35,6 +35,26 @@ def history_not_changed(proc): assert proc.expect([TIMEOUT, u'fuck']) +def select_command_with_arrows(proc): + """Ensures that command can be selected with arrow keys.""" + _set_confirmation(proc, True) + + proc.sendline(u'git h') + assert proc.expect([TIMEOUT, u"git: 'h' is not a git command."]) + + proc.sendline(u'fuck') + assert proc.expect([TIMEOUT, u'git show']) + proc.send('\033[B') + assert proc.expect([TIMEOUT, u'git push']) + proc.send('\033[B') + assert proc.expect([TIMEOUT, u'git help']) + proc.send('\033[A') + assert proc.expect([TIMEOUT, u'git push']) + proc.send('\n') + + assert proc.expect([TIMEOUT, u'Not a git repository']) + + def refuse_with_confirmation(proc): """Ensures that fix can be refused when confirmation enabled.""" _set_confirmation(proc, True) diff --git a/tests/functional/test_bash.py b/tests/functional/test_bash.py index a40361cd..d489708a 100644 --- a/tests/functional/test_bash.py +++ b/tests/functional/test_bash.py @@ -1,19 +1,20 @@ import pytest from tests.functional.plots import with_confirmation, without_confirmation, \ - refuse_with_confirmation, history_changed, history_not_changed + refuse_with_confirmation, history_changed, history_not_changed, \ + select_command_with_arrows from tests.functional.utils import spawn, functional, images containers = images(('ubuntu-python3-bash', u''' FROM ubuntu:latest RUN apt-get update -RUN apt-get install -yy python3 python3-pip python3-dev +RUN apt-get install -yy python3 python3-pip python3-dev git RUN pip3 install -U setuptools RUN ln -s /usr/bin/pip3 /usr/bin/pip '''), ('ubuntu-python2-bash', u''' FROM ubuntu:latest RUN apt-get update -RUN apt-get install -yy python python-pip python-dev +RUN apt-get install -yy python python-pip python-dev git RUN pip2 install -U pip setuptools ''')) @@ -31,7 +32,13 @@ def proc(request): @functional def test_with_confirmation(proc): with_confirmation(proc) - history_changed(proc) + history_changed(proc, u'echo test') + + +@functional +def test_select_command_with_arrows(proc): + select_command_with_arrows(proc) + history_changed(proc, u'git push') @functional @@ -43,4 +50,4 @@ def test_refuse_with_confirmation(proc): @functional def test_without_confirmation(proc): without_confirmation(proc) - history_changed(proc) + history_changed(proc, u'echo test') diff --git a/tests/functional/test_fish.py b/tests/functional/test_fish.py index d465faf3..5ff7f5c0 100644 --- a/tests/functional/test_fish.py +++ b/tests/functional/test_fish.py @@ -1,20 +1,22 @@ import pytest from tests.functional.plots import with_confirmation, without_confirmation, \ - refuse_with_confirmation + refuse_with_confirmation, select_command_with_arrows from tests.functional.utils import spawn, functional, images, bare containers = images(('ubuntu-python3-fish', u''' FROM ubuntu:latest RUN apt-get update -RUN apt-get install -yy python3 python3-pip python3-dev fish +RUN apt-get install -yy python3 python3-pip python3-dev fish git RUN pip3 install -U setuptools RUN ln -s /usr/bin/pip3 /usr/bin/pip +RUN apt-get install -yy fish '''), ('ubuntu-python2-fish', u''' FROM ubuntu:latest RUN apt-get update -RUN apt-get install -yy python python-pip python-dev fish +RUN apt-get install -yy python python-pip python-dev git RUN pip2 install -U pip setuptools +RUN apt-get install -yy fish ''')) @@ -34,6 +36,11 @@ def test_with_confirmation(proc): with_confirmation(proc) +@functional +def test_select_command_with_arrows(proc): + select_command_with_arrows(proc) + + @functional @pytest.mark.skipif( bool(bare), reason='https://github.com/travis-ci/apt-source-whitelist/issues/71') diff --git a/tests/functional/test_tcsh.py b/tests/functional/test_tcsh.py index 6dd926cc..75c3bb42 100644 --- a/tests/functional/test_tcsh.py +++ b/tests/functional/test_tcsh.py @@ -1,20 +1,22 @@ import pytest from tests.functional.utils import spawn, functional, images from tests.functional.plots import with_confirmation, without_confirmation, \ - refuse_with_confirmation + refuse_with_confirmation, select_command_with_arrows containers = images(('ubuntu-python3-tcsh', u''' FROM ubuntu:latest RUN apt-get update -RUN apt-get install -yy python3 python3-pip python3-dev tcsh +RUN apt-get install -yy python3 python3-pip python3-dev git RUN pip3 install -U setuptools RUN ln -s /usr/bin/pip3 /usr/bin/pip +RUN apt-get install -yy tcsh '''), ('ubuntu-python2-tcsh', u''' FROM ubuntu:latest RUN apt-get update -RUN apt-get install -yy python python-pip python-dev tcsh +RUN apt-get install -yy python python-pip python-dev git RUN pip2 install -U pip setuptools +RUN apt-get install -yy tcsh ''')) @@ -32,6 +34,11 @@ def test_with_confirmation(proc): with_confirmation(proc) +@functional +def test_select_command_with_arrows(proc): + select_command_with_arrows(proc) + + @functional def test_refuse_with_confirmation(proc): refuse_with_confirmation(proc) diff --git a/tests/functional/test_zsh.py b/tests/functional/test_zsh.py index 0201fa36..610cc23c 100644 --- a/tests/functional/test_zsh.py +++ b/tests/functional/test_zsh.py @@ -1,20 +1,22 @@ import pytest from tests.functional.utils import spawn, functional, images from tests.functional.plots import with_confirmation, without_confirmation, \ - refuse_with_confirmation, history_changed, history_not_changed + refuse_with_confirmation, history_changed, history_not_changed, select_command_with_arrows containers = images(('ubuntu-python3-zsh', u''' FROM ubuntu:latest RUN apt-get update -RUN apt-get install -yy python3 python3-pip python3-dev zsh +RUN apt-get install -yy python3 python3-pip python3-dev git RUN pip3 install -U setuptools RUN ln -s /usr/bin/pip3 /usr/bin/pip +RUN apt-get install -yy zsh '''), ('ubuntu-python2-zsh', u''' FROM ubuntu:latest RUN apt-get update -RUN apt-get install -yy python python-pip python-dev zsh +RUN apt-get install -yy python python-pip python-dev git RUN pip2 install -U pip setuptools +RUN apt-get install -yy zsh ''')) @@ -31,7 +33,13 @@ def proc(request): @functional def test_with_confirmation(proc): with_confirmation(proc) - history_changed(proc) + history_changed(proc, u'echo test') + + +@functional +def test_select_command_with_arrows(proc): + select_command_with_arrows(proc) + history_changed(proc, u'git push') @functional @@ -43,4 +51,4 @@ def test_refuse_with_confirmation(proc): @functional def test_without_confirmation(proc): without_confirmation(proc) - history_changed(proc) + history_changed(proc, u'echo test') diff --git a/thefuck/ui.py b/thefuck/ui.py index 257b6e66..f0b49793 100644 --- a/thefuck/ui.py +++ b/thefuck/ui.py @@ -90,14 +90,14 @@ def select_command(corrected_commands, settings): return selector.value selector.on_change(lambda val: logs.confirm_text(val, settings)) - for key in read_actions(): - if key == SELECT: + for action in read_actions(): + if action == SELECT: sys.stderr.write('\n') return selector.value - elif key == ABORT: + elif action == ABORT: logs.failed('\nAborted', settings) return - elif key == PREVIOUS: + elif action == PREVIOUS: selector.previous() - elif key == NEXT: + elif action == NEXT: selector.next() From da3bc60942c2d8ba475ae4efa41fc0acdbe05798 Mon Sep 17 00:00:00 2001 From: nvbn Date: Thu, 30 Jul 2015 18:39:41 +0300 Subject: [PATCH 10/14] #298 Fix arrow-tests on travis-ci --- tests/functional/test_fish.py | 2 ++ tests/functional/utils.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/functional/test_fish.py b/tests/functional/test_fish.py index 5ff7f5c0..c3368085 100644 --- a/tests/functional/test_fish.py +++ b/tests/functional/test_fish.py @@ -37,6 +37,8 @@ def test_with_confirmation(proc): @functional +@pytest.mark.skipif( + bool(bare), reason='https://github.com/travis-ci/apt-source-whitelist/issues/71') def test_select_command_with_arrows(proc): select_command_with_arrows(proc) diff --git a/tests/functional/utils.py b/tests/functional/utils.py index 2ca2b839..a1aeebb2 100644 --- a/tests/functional/utils.py +++ b/tests/functional/utils.py @@ -33,7 +33,7 @@ def spawn(request, tag, dockerfile, cmd): proc = pexpect.spawnu('docker run --volume {}:/src --tty=true ' '--interactive=true {} {}'.format(root, tag, cmd)) proc.sendline('pip install /src') - proc.sendline('cd /') + proc.sendline('cd /') proc.logfile = sys.stdout From 214acf56c53ca46634c4ad736a51f1f874bf1069 Mon Sep 17 00:00:00 2001 From: nvbn Date: Thu, 30 Jul 2015 20:04:40 +0300 Subject: [PATCH 11/14] #298 Wait before checking that history changed --- tests/functional/plots.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/functional/plots.py b/tests/functional/plots.py index 611742f2..1fb63904 100644 --- a/tests/functional/plots.py +++ b/tests/functional/plots.py @@ -1,3 +1,4 @@ +from time import sleep from pexpect import TIMEOUT @@ -25,12 +26,14 @@ def with_confirmation(proc): def history_changed(proc, to): """Ensures that history changed.""" + sleep(2) proc.send('\033[A') assert proc.expect([TIMEOUT, to]) def history_not_changed(proc): """Ensures that history not changed.""" + sleep(2) proc.send('\033[A') assert proc.expect([TIMEOUT, u'fuck']) From 8632a29edcb6071d881b07284535366e7659db9f Mon Sep 17 00:00:00 2001 From: nvbn Date: Fri, 31 Jul 2015 15:04:06 +0300 Subject: [PATCH 12/14] #298 Fix tests with `BARE` --- tests/functional/plots.py | 2 -- tests/functional/test_bash.py | 2 +- tests/functional/test_zsh.py | 2 +- 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/tests/functional/plots.py b/tests/functional/plots.py index 1fb63904..11b1f6be 100644 --- a/tests/functional/plots.py +++ b/tests/functional/plots.py @@ -26,14 +26,12 @@ def with_confirmation(proc): def history_changed(proc, to): """Ensures that history changed.""" - sleep(2) proc.send('\033[A') assert proc.expect([TIMEOUT, to]) def history_not_changed(proc): """Ensures that history not changed.""" - sleep(2) proc.send('\033[A') assert proc.expect([TIMEOUT, u'fuck']) diff --git a/tests/functional/test_bash.py b/tests/functional/test_bash.py index d489708a..2994d523 100644 --- a/tests/functional/test_bash.py +++ b/tests/functional/test_bash.py @@ -25,7 +25,7 @@ def proc(request): proc = spawn(request, tag, dockerfile, u'bash') proc.sendline(u"export PS1='$ '") proc.sendline(u'eval $(thefuck-alias)') - proc.sendline(u'touch $HISTFILE') + proc.sendline(u'echo > $HISTFILE') return proc diff --git a/tests/functional/test_zsh.py b/tests/functional/test_zsh.py index 610cc23c..7a30dd53 100644 --- a/tests/functional/test_zsh.py +++ b/tests/functional/test_zsh.py @@ -26,7 +26,7 @@ def proc(request): proc = spawn(request, tag, dockerfile, u'zsh') proc.sendline(u'eval $(thefuck-alias)') proc.sendline(u'export HISTFILE=~/.zsh_history') - proc.sendline(u'touch $HISTFILE') + proc.sendline(u'echo > $HISTFILE') return proc From cb2cddbdd96ebb4bc0b816f20e2723e4de67b2a6 Mon Sep 17 00:00:00 2001 From: nvbn Date: Fri, 31 Jul 2015 15:31:51 +0300 Subject: [PATCH 13/14] #298 Fix zsh tests with `BARE` --- tests/functional/test_zsh.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/functional/test_zsh.py b/tests/functional/test_zsh.py index 7a30dd53..c606a72e 100644 --- a/tests/functional/test_zsh.py +++ b/tests/functional/test_zsh.py @@ -27,6 +27,9 @@ def proc(request): proc.sendline(u'eval $(thefuck-alias)') proc.sendline(u'export HISTFILE=~/.zsh_history') proc.sendline(u'echo > $HISTFILE') + proc.sendline(u'export SAVEHIST=100') + proc.sendline(u'export HISTSIZE=100') + proc.sendline(u'setopt INC_APPEND_HISTORY') return proc From d442f959e960b00372f8f3f1eb8f1daa2c21f8b1 Mon Sep 17 00:00:00 2001 From: nvbn Date: Fri, 31 Jul 2015 15:36:08 +0300 Subject: [PATCH 14/14] #298 Update readme --- README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index f948e9ac..e7ba1380 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ E: Could not open lock file /var/lib/dpkg/lock - open (13: Permission denied) E: Unable to lock the administration directory (/var/lib/dpkg/), are you root? ➜ fuck -sudo apt-get install vim [enter/ctrl+c] +sudo apt-get install vim [enter/↑/↓/ctrl+c] [sudo] password for nvbn: Reading package lists... Done ... @@ -29,7 +29,7 @@ To push the current branch and set the remote as upstream, use ➜ fuck -git push --set-upstream origin master [enter/ctrl+c] +git push --set-upstream origin master [enter/↑/↓/ctrl+c] Counting objects: 9, done. ... ``` @@ -42,7 +42,7 @@ No command 'puthon' found, did you mean: zsh: command not found: puthon ➜ fuck -python [enter/ctrl+c] +python [enter/↑/↓/ctrl+c] Python 3.4.2 (default, Oct 8 2014, 13:08:17) ... ``` @@ -55,7 +55,7 @@ Did you mean this? branch ➜ fuck -git branch [enter/ctrl+c] +git branch [enter/↑/↓/ctrl+c] * master ``` @@ -67,7 +67,7 @@ Did you mean this? repl ➜ fuck -lein repl [enter/ctrl+c] +lein repl [enter/↑/↓/ctrl+c] nREPL server started on port 54848 on host 127.0.0.1 - nrepl://127.0.0.1:54848 REPL-y 0.3.1 ...