diff --git a/tests/test_corrector.py b/tests/test_corrector.py index cb817b77..0012a1fd 100644 --- a/tests/test_corrector.py +++ b/tests/test_corrector.py @@ -3,7 +3,8 @@ from pathlib import PosixPath, Path from mock import Mock from thefuck import corrector, conf from tests.utils import Rule, Command, CorrectedCommand -from thefuck.corrector import make_corrected_commands, get_corrected_commands, is_rule_enabled +from thefuck.corrector import make_corrected_commands, get_corrected_commands,\ + is_rule_enabled, organize_commands def test_load_rule(mocker): @@ -111,3 +112,13 @@ def test_get_corrected_commands(mocker): mocker.patch('thefuck.corrector.get_rules', return_value=rules) assert [cmd.script for cmd in get_corrected_commands(command)] \ == ['test!', 'test@', 'test;'] + + +def test_organize_commands(): + """Ensures that the function removes duplicates and sorts commands.""" + commands = [CorrectedCommand('ls'), CorrectedCommand('ls -la', priority=9000), + CorrectedCommand('ls -lh', priority=100), + CorrectedCommand('ls -lh', priority=9999)] + assert list(organize_commands(iter(commands))) \ + == [CorrectedCommand('ls'), CorrectedCommand('ls -lh', priority=100), + CorrectedCommand('ls -la', priority=9000)] diff --git a/tests/test_types.py b/tests/test_types.py index e0c1e682..be1cb8d7 100644 --- a/tests/test_types.py +++ b/tests/test_types.py @@ -1,37 +1,6 @@ -from thefuck.types import SortedCorrectedCommandsSequence from tests.utils import CorrectedCommand -class TestSortedCorrectedCommandsSequence(object): - def test_realises_generator_only_on_demand(self, settings): - should_realise = False - - def gen(): - yield CorrectedCommand('git commit') - yield CorrectedCommand('git branch', priority=200) - assert should_realise - yield CorrectedCommand('git checkout', priority=100) - - commands = SortedCorrectedCommandsSequence(gen()) - 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): - side_effect = lambda *_: None - seq = SortedCorrectedCommandsSequence( - iter([CorrectedCommand('ls', priority=100), - CorrectedCommand('ls', priority=200), - CorrectedCommand('ls', side_effect, 300)])) - assert set(seq) == {CorrectedCommand('ls', priority=100), - CorrectedCommand('ls', side_effect, 300)} - - def test_with_blank(self): - seq = SortedCorrectedCommandsSequence(iter([])) - assert list(seq) == [] - - class TestCorrectedCommand(object): def test_equality(self): diff --git a/tests/test_ui.py b/tests/test_ui.py index d18fa018..731171cc 100644 --- a/tests/test_ui.py +++ b/tests/test_ui.py @@ -1,10 +1,9 @@ # -*- encoding: utf-8 -*- -from mock import Mock import pytest from itertools import islice from thefuck import ui -from thefuck.types import CorrectedCommand, SortedCorrectedCommandsSequence +from thefuck.types import CorrectedCommand @pytest.fixture @@ -41,7 +40,7 @@ def test_read_actions(patch_getch): def test_command_selector(): - selector = ui.CommandSelector([1, 2, 3]) + selector = ui.CommandSelector(iter([1, 2, 3])) assert selector.value == 1 selector.next() assert selector.value == 2 @@ -57,51 +56,49 @@ def test_command_selector(): class TestSelectCommand(object): @pytest.fixture def commands_with_side_effect(self): - return SortedCorrectedCommandsSequence( - iter([CorrectedCommand('ls', lambda *_: None, 100), - CorrectedCommand('cd', lambda *_: None, 100)])) + return [CorrectedCommand('ls', lambda *_: None, 100), + CorrectedCommand('cd', lambda *_: None, 100)] @pytest.fixture def commands(self): - return SortedCorrectedCommandsSequence( - iter([CorrectedCommand('ls', None, 100), - CorrectedCommand('cd', None, 100)])) + return [CorrectedCommand('ls', None, 100), + CorrectedCommand('cd', None, 100)] def test_without_commands(self, capsys): - assert ui.select_command([]) is None + assert ui.select_command(iter([])) is None assert capsys.readouterr() == ('', 'No fucks given\n') def test_without_confirmation(self, capsys, commands, settings): settings.require_confirmation = False - assert ui.select_command(commands) == commands[0] + assert ui.select_command(iter(commands)) == commands[0] assert capsys.readouterr() == ('', 'ls\n') def test_without_confirmation_with_side_effects( self, capsys, commands_with_side_effect, settings): settings.require_confirmation = False - assert ui.select_command(commands_with_side_effect) \ + assert ui.select_command(iter(commands_with_side_effect)) \ == 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) == commands[0] + assert ui.select_command(iter(commands)) == 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) is None + assert ui.select_command(iter(commands)) is None 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): patch_getch(['\n']) - assert ui.select_command(commands_with_side_effect)\ + assert ui.select_command(iter(commands_with_side_effect))\ == commands_with_side_effect[0] 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']) - assert ui.select_command(commands) == commands[1] + assert ui.select_command(iter(commands)) == commands[1] assert capsys.readouterr() == ( '', u'\x1b[1K\rls [enter/↑/↓/ctrl+c]\x1b[1K\rcd [enter/↑/↓/ctrl+c]\n') diff --git a/thefuck/corrector.py b/thefuck/corrector.py index acc82bef..3fd2cc21 100644 --- a/thefuck/corrector.py +++ b/thefuck/corrector.py @@ -2,7 +2,7 @@ import sys from imp import load_source from pathlib import Path from .conf import settings, DEFAULT_PRIORITY, ALL_ENABLED -from .types import Rule, CorrectedCommand, SortedCorrectedCommandsSequence +from .types import Rule, CorrectedCommand from .utils import compatibility_call from . import logs @@ -76,10 +76,33 @@ def make_corrected_commands(command, rule): side_effect=rule.side_effect, priority=(n + 1) * rule.priority) +def organize_commands(corrected_commands): + """Yields sorted commands without duplicates.""" + try: + first_command = next(corrected_commands) + yield first_command + except StopIteration: + return + + without_duplicates = { + command for command in sorted( + corrected_commands, key=lambda command: command.priority) + if command != first_command} + + sorted_commands = sorted( + without_duplicates, + key=lambda corrected_command: corrected_command.priority) + + logs.debug('Corrected commands: '.format( + ', '.join(str(cmd) for cmd in [first_command] + sorted_commands))) + + for command in sorted_commands: + yield command + def get_corrected_commands(command): corrected_commands = ( corrected for rule in get_rules() if is_rule_match(command, rule) for corrected in make_corrected_commands(command, rule)) - return SortedCorrectedCommandsSequence(corrected_commands) + return organize_commands(corrected_commands) diff --git a/thefuck/types.py b/thefuck/types.py index b3ebddce..4d9d5e91 100644 --- a/thefuck/types.py +++ b/thefuck/types.py @@ -1,5 +1,4 @@ from collections import namedtuple -from traceback import format_stack Command = namedtuple('Command', ('script', 'stdout', 'stderr')) @@ -36,62 +35,3 @@ class Settings(dict): def __setattr__(self, key, value): self[key] = value - - -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): - self._commands = commands - self._cached = self._realise_first() - self._realised = False - - def _realise_first(self): - try: - return [next(self._commands)] - except StopIteration: - return [] - - def _remove_duplicates(self, corrected_commands): - """Removes low-priority duplicates.""" - commands = {command - for command in sorted(corrected_commands, - key=lambda command: -command.priority) - if command.script != self._cached[0]} - return commands - - def _realise(self): - """Realises generator, removes duplicates and sorts commands.""" - from .logs import debug - - if self._cached: - commands = self._remove_duplicates(self._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()))) - - 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) diff --git a/thefuck/ui.py b/thefuck/ui.py index 8f71872e..d04bea8d 100644 --- a/thefuck/ui.py +++ b/thefuck/ui.py @@ -50,14 +50,25 @@ def read_actions(): class CommandSelector(object): + """Helper for selecting rule from rules list.""" + def __init__(self, commands): - self._commands = commands + self._commands_gen = commands + self._commands = [next(self._commands_gen)] + self._realised = False self._index = 0 + def _realise(self): + if not self._realised: + self._commands += list(self._commands_gen) + self._realised = True + def next(self): + self._realise() self._index = (self._index + 1) % len(self._commands) def previous(self): + self._realise() self._index = (self._index - 1) % len(self._commands) @property @@ -73,11 +84,12 @@ def select_command(corrected_commands): - selected command. """ - if not corrected_commands: + try: + selector = CommandSelector(corrected_commands) + except StopIteration: logs.failed('No fucks given') return - selector = CommandSelector(corrected_commands) if not settings.require_confirmation: logs.show_corrected_command(selector.value) return selector.value