1
0
mirror of https://github.com/nvbn/thefuck.git synced 2025-02-21 20:38:54 +00:00

Move rule-related code to Rule

This commit is contained in:
nvbn 2015-09-08 14:18:11 +03:00
parent bf80d97062
commit 4a27595e97
7 changed files with 185 additions and 156 deletions

View File

@ -2,7 +2,6 @@ import pytest
import os import os
from thefuck.rules.fix_file import match, get_new_command from thefuck.rules.fix_file import match, get_new_command
from tests.utils import Command from tests.utils import Command
from thefuck.types import Settings
# (script, file, line, col (or None), stdout, stderr) # (script, file, line, col (or None), stdout, stderr)

View File

@ -1,42 +1,8 @@
import pytest import pytest
from pathlib import PosixPath, Path from pathlib import PosixPath
from mock import Mock
from thefuck import corrector, conf from thefuck import corrector, conf
from tests.utils import Rule, Command, CorrectedCommand from tests.utils import Rule, Command, CorrectedCommand
from thefuck.corrector import make_corrected_commands, get_corrected_commands,\ from thefuck.corrector import get_corrected_commands, organize_commands
is_rule_enabled, organize_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')) \
== Rule('bash', match, get_new_command, priority=900)
load_source.assert_called_once_with('bash', '/rules/bash.py')
@pytest.mark.parametrize('rules, exclude_rules, rule, is_enabled', [
(conf.DEFAULT_RULES, [], Rule('git', enabled_by_default=True), True),
(conf.DEFAULT_RULES, [], Rule('git', enabled_by_default=False), False),
([], [], Rule('git', enabled_by_default=False), False),
([], [], Rule('git', enabled_by_default=True), False),
(conf.DEFAULT_RULES + ['git'], [], Rule('git', enabled_by_default=False), True),
(['git'], [], Rule('git', enabled_by_default=False), True),
(conf.DEFAULT_RULES, ['git'], Rule('git', enabled_by_default=True), False),
(conf.DEFAULT_RULES, ['git'], Rule('git', enabled_by_default=False), False),
([], ['git'], Rule('git', enabled_by_default=True), False),
([], ['git'], Rule('git', enabled_by_default=False), False)])
def test_is_rule_enabled(settings, rules, exclude_rules, rule, is_enabled):
settings.update(rules=rules,
exclude_rules=exclude_rules)
assert is_rule_enabled(rule) == is_enabled
class TestGetRules(object): class TestGetRules(object):
@ -49,7 +15,7 @@ class TestGetRules(object):
@pytest.fixture(autouse=True) @pytest.fixture(autouse=True)
def load_source(self, monkeypatch): def load_source(self, monkeypatch):
monkeypatch.setattr('thefuck.corrector.load_source', monkeypatch.setattr('thefuck.types.load_source',
lambda x, _: Rule(x)) lambda x, _: Rule(x))
def _compare_names(self, rules, names): def _compare_names(self, rules, names):
@ -70,37 +36,6 @@ class TestGetRules(object):
self._compare_names(rules, loaded_rules) self._compare_names(rules, loaded_rules)
class TestIsRuleMatch(object):
def test_no_match(self):
assert not corrector.is_rule_match(
Command('ls'), Rule('', lambda _: False))
def test_match(self):
rule = Rule('', lambda x: x.script == 'cd ..')
assert corrector.is_rule_match(Command('cd ..'), rule)
@pytest.mark.usefixtures('no_colors')
def test_when_rule_failed(self, capsys):
rule = Rule('test', Mock(side_effect=OSError('Denied')),
requires_output=False)
assert not corrector.is_rule_match(Command('ls'), rule)
assert capsys.readouterr()[1].split('\n')[0] == '[WARN] Rule test:'
class TestMakeCorrectedCommands(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)) \
== [CorrectedCommand(script='test!', priority=100),
CorrectedCommand(script='test@', priority=200)]
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)) \
== [CorrectedCommand(script='test!', priority=100)]
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),

View File

@ -1,4 +1,8 @@
from tests.utils import CorrectedCommand from mock import Mock
from pathlib import Path
import pytest
from tests.utils import CorrectedCommand, Rule, Command
from thefuck import conf
class TestCorrectedCommand(object): class TestCorrectedCommand(object):
@ -12,3 +16,63 @@ class TestCorrectedCommand(object):
def test_hashable(self): def test_hashable(self):
assert {CorrectedCommand('ls', None, 100), assert {CorrectedCommand('ls', None, 100),
CorrectedCommand('ls', None, 200)} == {CorrectedCommand('ls')} CorrectedCommand('ls', None, 200)} == {CorrectedCommand('ls')}
class TestRule(object):
def test_from_path(self, mocker):
match = object()
get_new_command = object()
load_source = mocker.patch(
'thefuck.types.load_source',
return_value=Mock(match=match,
get_new_command=get_new_command,
enabled_by_default=True,
priority=900,
requires_output=True))
assert Rule.from_path(Path('/rules/bash.py')) \
== Rule('bash', match, get_new_command, priority=900)
load_source.assert_called_once_with('bash', '/rules/bash.py')
@pytest.mark.parametrize('rules, exclude_rules, rule, is_enabled', [
(conf.DEFAULT_RULES, [], Rule('git', enabled_by_default=True), True),
(conf.DEFAULT_RULES, [], Rule('git', enabled_by_default=False), False),
([], [], Rule('git', enabled_by_default=False), False),
([], [], Rule('git', enabled_by_default=True), False),
(conf.DEFAULT_RULES + ['git'], [], Rule('git', enabled_by_default=False), True),
(['git'], [], Rule('git', enabled_by_default=False), True),
(conf.DEFAULT_RULES, ['git'], Rule('git', enabled_by_default=True), False),
(conf.DEFAULT_RULES, ['git'], Rule('git', enabled_by_default=False), False),
([], ['git'], Rule('git', enabled_by_default=True), False),
([], ['git'], Rule('git', enabled_by_default=False), False)])
def test_is_enabled(self, settings, rules, exclude_rules, rule, is_enabled):
settings.update(rules=rules,
exclude_rules=exclude_rules)
assert rule.is_enabled == is_enabled
def test_isnt_match(self):
assert not Rule('', lambda _: False).is_match(
Command('ls'))
def test_is_match(self):
rule = Rule('', lambda x: x.script == 'cd ..')
assert rule.is_match(Command('cd ..'))
@pytest.mark.usefixtures('no_colors')
def test_isnt_match_when_rule_failed(self, capsys):
rule = Rule('test', Mock(side_effect=OSError('Denied')),
requires_output=False)
assert not rule.is_match(Command('ls'))
assert capsys.readouterr()[1].split('\n')[0] == '[WARN] Rule test:'
def test_get_corrected_commands_with_rule_returns_list(self):
rule = Rule(get_new_command=lambda x: [x.script + '!', x.script + '@'],
priority=100)
assert list(rule.get_corrected_commands(Command(script='test'))) \
== [CorrectedCommand(script='test!', priority=100),
CorrectedCommand(script='test@', priority=200)]
def test_get_corrected_commands_with_rule_returns_command(self):
rule = Rule(get_new_command=lambda x: x.script + '!',
priority=100)
assert list(rule.get_corrected_commands(Command(script='test'))) \
== [CorrectedCommand(script='test!', priority=100)]

View File

@ -7,19 +7,22 @@ def Command(script='', stdout='', stderr=''):
return types.Command(script, stdout, stderr) return types.Command(script, stdout, stderr)
def Rule(name='', match=lambda *_: True, class Rule(types.Rule):
get_new_command=lambda *_: '', def __init__(self, name='', match=lambda *_: True,
enabled_by_default=True, get_new_command=lambda *_: '',
side_effect=None, enabled_by_default=True,
priority=DEFAULT_PRIORITY, side_effect=None,
requires_output=True): priority=DEFAULT_PRIORITY,
return types.Rule(name, match, get_new_command, requires_output=True):
enabled_by_default, side_effect, super(Rule, self).__init__(name, match, get_new_command,
priority, requires_output) enabled_by_default, side_effect,
priority, requires_output)
def CorrectedCommand(script='', side_effect=None, priority=DEFAULT_PRIORITY): class CorrectedCommand(types.CorrectedCommand):
return types.CorrectedCommand(script, side_effect, priority) def __init__(self, script='', side_effect=None, priority=DEFAULT_PRIORITY):
super(CorrectedCommand, self).__init__(
script, side_effect, priority)
root = Path(__file__).parent.parent.resolve() root = Path(__file__).parent.parent.resolve()

View File

@ -2,7 +2,15 @@ from imp import load_source
import os import os
import sys import sys
from six import text_type from six import text_type
from .types import Settings
class Settings(dict):
def __getattr__(self, item):
return self.get(item)
def __setattr__(self, key, value):
self[key] = value
ALL_ENABLED = object() ALL_ENABLED = object()
DEFAULT_RULES = [ALL_ENABLED] DEFAULT_RULES = [ALL_ENABLED]

View File

@ -1,45 +1,16 @@
import sys
from imp import load_source
from pathlib import Path from pathlib import Path
from .conf import settings, DEFAULT_PRIORITY, ALL_ENABLED from .conf import settings
from .types import Rule, CorrectedCommand from .types import Rule
from .utils import compatibility_call
from . import logs from . import logs
def load_rule(rule): def get_loaded_rules(rules_paths):
"""Imports rule module and returns it."""
name = rule.name[:-3]
with logs.debug_time(u'Importing rule: {};'.format(name)):
rule_module = load_source(name, str(rule))
priority = getattr(rule_module, 'priority', DEFAULT_PRIORITY)
return 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 is_rule_enabled(rule):
"""Returns `True` when rule enabled."""
if rule.name in settings.exclude_rules:
return False
elif rule.name in settings.rules:
return True
elif rule.enabled_by_default and ALL_ENABLED in settings.rules:
return True
else:
return False
def get_loaded_rules(rules):
"""Yields all available rules.""" """Yields all available rules."""
for rule in rules: for path in rules_paths:
if rule.name != '__init__.py': if path.name != '__init__.py':
loaded_rule = load_rule(rule) rule = Rule.from_path(path)
if is_rule_enabled(loaded_rule): if rule.is_enabled:
yield loaded_rule yield rule
def get_rules(): def get_rules():
@ -52,30 +23,6 @@ def get_rules():
key=lambda rule: rule.priority) key=lambda rule: rule.priority)
def is_rule_match(command, rule):
"""Returns first matched rule for command."""
script_only = command.stdout is None and command.stderr is None
if script_only and rule.requires_output:
return False
try:
with logs.debug_time(u'Trying rule: {};'.format(rule.name)):
if compatibility_call(rule.match, command):
return True
except Exception:
logs.rule_failed(rule, sys.exc_info())
def make_corrected_commands(command, rule):
new_commands = compatibility_call(rule.get_new_command, command)
if not isinstance(new_commands, list):
new_commands = (new_commands,)
for n, new_command in enumerate(new_commands):
yield CorrectedCommand(script=new_command,
side_effect=rule.side_effect,
priority=(n + 1) * rule.priority)
def organize_commands(corrected_commands): def organize_commands(corrected_commands):
"""Yields sorted commands without duplicates.""" """Yields sorted commands without duplicates."""
try: try:
@ -103,6 +50,6 @@ def organize_commands(corrected_commands):
def get_corrected_commands(command): def get_corrected_commands(command):
corrected_commands = ( corrected_commands = (
corrected for rule in get_rules() corrected for rule in get_rules()
if is_rule_match(command, rule) if rule.is_match(command)
for corrected in make_corrected_commands(command, rule)) for corrected in rule.get_corrected_commands(command))
return organize_commands(corrected_commands) return organize_commands(corrected_commands)

View File

@ -1,10 +1,91 @@
from collections import namedtuple from collections import namedtuple
from imp import load_source
import sys
from .conf import settings, DEFAULT_PRIORITY, ALL_ENABLED
from .utils import compatibility_call
from . import logs
Command = namedtuple('Command', ('script', 'stdout', 'stderr')) Command = namedtuple('Command', ('script', 'stdout', 'stderr'))
Rule = namedtuple('Rule', ('name', 'match', 'get_new_command',
'enabled_by_default', 'side_effect', class Rule(object):
'priority', 'requires_output')) def __init__(self, name, match, get_new_command,
enabled_by_default, side_effect,
priority, requires_output):
self.name = name
self.match = match
self.get_new_command = get_new_command
self.enabled_by_default = enabled_by_default
self.side_effect = side_effect
self.priority = priority
self.requires_output = requires_output
def __eq__(self, other):
if isinstance(other, Rule):
return (self.name, self.match, self.get_new_command,
self.enabled_by_default, self.side_effect,
self.priority, self.requires_output) \
== (other.name, other.match, other.get_new_command,
other.enabled_by_default, other.side_effect,
other.priority, other.requires_output)
else:
return False
def __repr__(self):
return 'Rule(name={}, match={}, get_new_command={}, ' \
'enabled_by_default={}, side_effect={}, ' \
'priority={}, requires_output)'.format(
self.name, self.match, self.get_new_command,
self.enabled_by_default, self.side_effect,
self.priority, self.requires_output)
@classmethod
def from_path(cls, path):
"""Creates rule instance from path."""
name = path.name[:-3]
with logs.debug_time(u'Importing rule: {};'.format(name)):
rule_module = load_source(name, str(path))
priority = getattr(rule_module, 'priority', DEFAULT_PRIORITY)
return cls(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))
@property
def is_enabled(self):
if self.name in settings.exclude_rules:
return False
elif self.name in settings.rules:
return True
elif self.enabled_by_default and ALL_ENABLED in settings.rules:
return True
else:
return False
def is_match(self, command):
"""Returns `True` if rule matches the command."""
script_only = command.stdout is None and command.stderr is None
if script_only and self.requires_output:
return False
try:
with logs.debug_time(u'Trying rule: {};'.format(self.name)):
if compatibility_call(self.match, command):
return True
except Exception:
logs.rule_failed(self, sys.exc_info())
def get_corrected_commands(self, command):
new_commands = compatibility_call(self.get_new_command, command)
if not isinstance(new_commands, list):
new_commands = (new_commands,)
for n, new_command in enumerate(new_commands):
yield CorrectedCommand(script=new_command,
side_effect=self.side_effect,
priority=(n + 1) * self.priority)
class CorrectedCommand(object): class CorrectedCommand(object):
@ -27,11 +108,3 @@ class CorrectedCommand(object):
def __repr__(self): def __repr__(self):
return 'CorrectedCommand(script={}, side_effect={}, priority={})'.format( return 'CorrectedCommand(script={}, side_effect={}, priority={})'.format(
self.script, self.side_effect, self.priority) self.script, self.side_effect, self.priority)
class Settings(dict):
def __getattr__(self, item):
return self.get(item)
def __setattr__(self, key, value):
self[key] = value