From 12394ca8423a438915fed996383b44471fc1139d Mon Sep 17 00:00:00 2001 From: nvbn Date: Tue, 1 Sep 2015 12:51:41 +0300 Subject: [PATCH] #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 e7f55e9..aa232a1 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 0496e61..7142aa1 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 41d0f10..910bbc9 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 d822ebe..f919963 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 4171358..981c0fe 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 2e5b41a..6da4234 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 146cbd7..31f0316 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')