1
0
mirror of https://github.com/nvbn/thefuck.git synced 2024-10-06 02:41:10 +01:00

#334: Don't wait for all rules before showing result

This commit is contained in:
nvbn 2015-09-01 12:51:41 +03:00
parent ebe53f0d18
commit 12394ca842
7 changed files with 132 additions and 46 deletions

View File

@ -1,6 +1,12 @@
import pytest import pytest
from mock import Mock
@pytest.fixture @pytest.fixture
def no_memoize(monkeypatch): def no_memoize(monkeypatch):
monkeypatch.setattr('thefuck.utils.memoize.disabled', True) monkeypatch.setattr('thefuck.utils.memoize.disabled', True)
@pytest.fixture
def settings():
return Mock(debug=False, no_colors=True)

View File

@ -3,7 +3,7 @@ from pathlib import PosixPath, Path
from mock import Mock from mock import Mock
from thefuck import corrector, conf, types from thefuck import corrector, conf, types
from tests.utils import Rule, Command, CorrectedCommand 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): def test_load_rule(mocker):
@ -75,15 +75,6 @@ class TestGetCorrectedCommands(object):
== [CorrectedCommand(script='test!', priority=100)] == [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): def test_get_corrected_commands(mocker):
command = Command('test', 'test', 'test') command = Command('test', 'test', 'test')
rules = [Rule(match=lambda *_: False), rules = [Rule(match=lambda *_: False),
@ -94,4 +85,4 @@ def test_get_corrected_commands(mocker):
priority=60)] priority=60)]
mocker.patch('thefuck.corrector.get_rules', return_value=rules) mocker.patch('thefuck.corrector.get_rules', return_value=rules)
assert [cmd.script for cmd in get_corrected_commands(command, None, Mock(debug=False))] \ assert [cmd.script for cmd in get_corrected_commands(command, None, Mock(debug=False))] \
== ['test@', 'test!', 'test;'] == ['test!', 'test@', 'test;']

View File

@ -1,5 +1,6 @@
from thefuck.types import RulesNamesList, Settings from thefuck.types import RulesNamesList, Settings, \
from tests.utils import Rule SortedCorrectedCommandsSequence
from tests.utils import Rule, CorrectedCommand
def test_rules_names_list(): def test_rules_names_list():
@ -15,3 +16,31 @@ def test_update_settings():
assert new_settings.key == 'val' assert new_settings.key == 'val'
assert new_settings.unset == 'unset-value' assert new_settings.unset == 'unset-value'
assert settings.key == 'val' 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)}

View File

@ -4,7 +4,7 @@ from mock import Mock
import pytest import pytest
from itertools import islice from itertools import islice
from thefuck import ui from thefuck import ui
from thefuck.types import CorrectedCommand from thefuck.types import CorrectedCommand, SortedCorrectedCommandsSequence
@pytest.fixture @pytest.fixture
@ -58,14 +58,18 @@ def test_command_selector():
class TestSelectCommand(object): class TestSelectCommand(object):
@pytest.fixture @pytest.fixture
def commands_with_side_effect(self): def commands_with_side_effect(self, settings):
return [CorrectedCommand('ls', lambda *_: None, 100), return SortedCorrectedCommandsSequence(
CorrectedCommand('cd', lambda *_: None, 100)] iter([CorrectedCommand('ls', lambda *_: None, 100),
CorrectedCommand('cd', lambda *_: None, 100)]),
settings)
@pytest.fixture @pytest.fixture
def commands(self): def commands(self, settings):
return [CorrectedCommand('ls', None, 100), return SortedCorrectedCommandsSequence(
CorrectedCommand('cd', None, 100)] iter([CorrectedCommand('ls', None, 100),
CorrectedCommand('cd', None, 100)]),
settings)
def test_without_commands(self, capsys): def test_without_commands(self, capsys):
assert ui.select_command([], Mock(debug=False, no_color=True)) is None assert ui.select_command([], Mock(debug=False, no_color=True)) is None
@ -92,9 +96,11 @@ class TestSelectCommand(object):
require_confirmation=True)) == commands[0] require_confirmation=True)) == commands[0]
assert capsys.readouterr() == ('', u'\x1b[1K\rls [enter/↑/↓/ctrl+c]\n') 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']) 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, Mock(debug=False, no_color=True,
require_confirmation=True)) == commands[0] require_confirmation=True)) == commands[0]
assert capsys.readouterr() == ('', u'\x1b[1K\rls [enter/ctrl+c]\n') assert capsys.readouterr() == ('', u'\x1b[1K\rls [enter/ctrl+c]\n')

View File

@ -2,7 +2,6 @@ import sys
from imp import load_source from imp import load_source
from pathlib import Path from pathlib import Path
from . import conf, types, logs from . import conf, types, logs
from .utils import eager
def load_rule(rule, settings): def load_rule(rule, settings):
@ -27,17 +26,16 @@ def get_loaded_rules(rules, settings):
yield loaded_rule yield loaded_rule
@eager
def get_rules(user_dir, settings): def get_rules(user_dir, settings):
"""Returns all enabled rules.""" """Returns all enabled rules."""
bundled = Path(__file__).parent \ bundled = Path(__file__).parent \
.joinpath('rules') \ .joinpath('rules') \
.glob('*.py') .glob('*.py')
user = user_dir.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): def get_matched_rules(command, rules, settings):
"""Returns first matched rule for command.""" """Returns first matched rule for command."""
script_only = command.stdout is None and command.stderr is None 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) 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): def get_corrected_commands(command, user_dir, settings):
rules = 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 = 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)
corrected_commands = make_corrected_commands(command, matched, settings) corrected_commands = make_corrected_commands(command, matched, settings)
return sorted(remove_duplicates(corrected_commands), return types.SortedCorrectedCommandsSequence(corrected_commands, settings)
key=lambda corrected_command: corrected_command.priority)

View File

@ -1,5 +1,6 @@
from collections import namedtuple from collections import namedtuple
from traceback import format_stack
from .logs import debug
Command = namedtuple('Command', ('script', 'stdout', 'stderr')) Command = namedtuple('Command', ('script', 'stdout', 'stderr'))
@ -18,7 +19,6 @@ class RulesNamesList(list):
class Settings(dict): class Settings(dict):
def __getattr__(self, item): def __getattr__(self, item):
return self.get(item) return self.get(item)
@ -29,3 +29,73 @@ class Settings(dict):
conf = dict(kwargs) conf = dict(kwargs)
conf.update(self) conf.update(self)
return Settings(conf) 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

View File

@ -88,9 +88,9 @@ def select_command(corrected_commands, settings):
logs.show_corrected_command(selector.value, settings) logs.show_corrected_command(selector.value, settings)
return selector.value return selector.value
multiple_cmds = len(corrected_commands) > 1 selector.on_change(
lambda val: logs.confirm_text(val, corrected_commands.is_multiple,
selector.on_change(lambda val: logs.confirm_text(val, multiple_cmds, settings)) settings))
for action in read_actions(): for action in read_actions():
if action == SELECT: if action == SELECT:
sys.stderr.write('\n') sys.stderr.write('\n')