mirror of
				https://github.com/nvbn/thefuck.git
				synced 2025-10-30 22:54:14 +00:00 
			
		
		
		
	#334: Don't wait for all rules before showing result
This commit is contained in:
		| @@ -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) | ||||||
|   | |||||||
| @@ -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;'] | ||||||
|   | |||||||
| @@ -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)} | ||||||
|   | |||||||
| @@ -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') | ||||||
|   | |||||||
| @@ -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) |  | ||||||
|   | |||||||
| @@ -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 | ||||||
|   | |||||||
| @@ -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') | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user