1
0
mirror of https://github.com/nvbn/thefuck.git synced 2025-01-31 02:01:13 +00:00

Merge pull request #366 from nvbn/unned-abstractions

Improve code structure
This commit is contained in:
Vladimir Iakovlev 2015-09-08 17:53:37 +03:00
commit 6e886c6b4f
20 changed files with 620 additions and 542 deletions

View File

@ -41,3 +41,8 @@ def functional(request):
if request.node.get_marker('functional') \ if request.node.get_marker('functional') \
and not request.config.getoption('enable_functional'): and not request.config.getoption('enable_functional'):
pytest.skip('functional tests are disabled') pytest.skip('functional tests are disabled')
@pytest.fixture
def source_root():
return Path(__file__).parent.parent.resolve()

View File

@ -31,30 +31,34 @@ def proc(request, spawnu, run_without_docker):
@pytest.mark.functional @pytest.mark.functional
@pytest.mark.once_without_docker @pytest.mark.once_without_docker
def test_with_confirmation(proc, TIMEOUT): def test_with_confirmation(proc, TIMEOUT, run_without_docker):
with_confirmation(proc, TIMEOUT) with_confirmation(proc, TIMEOUT)
history_changed(proc, TIMEOUT, u'echo test') if not run_without_docker:
history_changed(proc, TIMEOUT, u'echo test')
@pytest.mark.functional @pytest.mark.functional
@pytest.mark.once_without_docker @pytest.mark.once_without_docker
def test_select_command_with_arrows(proc, TIMEOUT): def test_select_command_with_arrows(proc, TIMEOUT, run_without_docker):
select_command_with_arrows(proc, TIMEOUT) select_command_with_arrows(proc, TIMEOUT)
history_changed(proc, TIMEOUT, u'git help') if not run_without_docker:
history_changed(proc, TIMEOUT, u'git help')
@pytest.mark.functional @pytest.mark.functional
@pytest.mark.once_without_docker @pytest.mark.once_without_docker
def test_refuse_with_confirmation(proc, TIMEOUT): def test_refuse_with_confirmation(proc, TIMEOUT, run_without_docker):
refuse_with_confirmation(proc, TIMEOUT) refuse_with_confirmation(proc, TIMEOUT)
history_not_changed(proc, TIMEOUT) if not run_without_docker:
history_not_changed(proc, TIMEOUT)
@pytest.mark.functional @pytest.mark.functional
@pytest.mark.once_without_docker @pytest.mark.once_without_docker
def test_without_confirmation(proc, TIMEOUT): def test_without_confirmation(proc, TIMEOUT, run_without_docker):
without_confirmation(proc, TIMEOUT) without_confirmation(proc, TIMEOUT)
history_changed(proc, TIMEOUT, u'echo test') if not run_without_docker:
history_changed(proc, TIMEOUT, u'echo test')
@pytest.mark.functional @pytest.mark.functional

View File

@ -1,5 +1,5 @@
import pytest import pytest
from thefuck.main import _get_current_version from thefuck.utils import get_installation_info
envs = ((u'bash', 'thefuck/ubuntu-bash', u''' envs = ((u'bash', 'thefuck/ubuntu-bash', u'''
FROM ubuntu:latest FROM ubuntu:latest
@ -18,7 +18,8 @@ def test_installation(spawnu, shell, TIMEOUT, tag, dockerfile):
proc = spawnu(tag, dockerfile, shell) proc = spawnu(tag, dockerfile, shell)
proc.sendline(u'cat /src/install.sh | sh - && $0') proc.sendline(u'cat /src/install.sh | sh - && $0')
proc.sendline(u'thefuck --version') proc.sendline(u'thefuck --version')
assert proc.expect([TIMEOUT, u'thefuck {}'.format(_get_current_version())], version = get_installation_info().version
assert proc.expect([TIMEOUT, u'thefuck {}'.format(version)],
timeout=600) timeout=600)
proc.sendline(u'fuck') proc.sendline(u'fuck')
assert proc.expect([TIMEOUT, u'No fucks given']) assert proc.expect([TIMEOUT, u'No fucks given'])

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

@ -2,15 +2,6 @@ import pytest
import six import six
from mock import Mock from mock import Mock
from thefuck import conf from thefuck import conf
from tests.utils import Rule
@pytest.mark.parametrize('enabled, rules, result', [
(True, conf.DEFAULT_RULES, True),
(False, conf.DEFAULT_RULES, False),
(False, conf.DEFAULT_RULES + ['test'], True)])
def test_default(enabled, rules, result):
assert (Rule('test', enabled_by_default=enabled) in rules) == result
@pytest.fixture @pytest.fixture
@ -28,7 +19,7 @@ def environ(monkeypatch):
@pytest.mark.usefixture('environ') @pytest.mark.usefixture('environ')
def test_settings_defaults(load_source, settings): def test_settings_defaults(load_source, settings):
load_source.return_value = object() load_source.return_value = object()
conf.init_settings(Mock()) settings.init()
for key, val in conf.DEFAULT_SETTINGS.items(): for key, val in conf.DEFAULT_SETTINGS.items():
assert getattr(settings, key) == val assert getattr(settings, key) == val
@ -42,7 +33,7 @@ class TestSettingsFromFile(object):
no_colors=True, no_colors=True,
priority={'vim': 100}, priority={'vim': 100},
exclude_rules=['git']) exclude_rules=['git'])
conf.init_settings(Mock()) settings.init()
assert settings.rules == ['test'] assert settings.rules == ['test']
assert settings.wait_command == 10 assert settings.wait_command == 10
assert settings.require_confirmation is True assert settings.require_confirmation is True
@ -56,7 +47,7 @@ class TestSettingsFromFile(object):
exclude_rules=[], exclude_rules=[],
require_confirmation=True, require_confirmation=True,
no_colors=True) no_colors=True)
conf.init_settings(Mock()) settings.init()
assert settings.rules == conf.DEFAULT_RULES + ['test'] assert settings.rules == conf.DEFAULT_RULES + ['test']
@ -69,7 +60,7 @@ class TestSettingsFromEnv(object):
'THEFUCK_REQUIRE_CONFIRMATION': 'true', 'THEFUCK_REQUIRE_CONFIRMATION': 'true',
'THEFUCK_NO_COLORS': 'false', 'THEFUCK_NO_COLORS': 'false',
'THEFUCK_PRIORITY': 'bash=10:lisp=wrong:vim=15'}) 'THEFUCK_PRIORITY': 'bash=10:lisp=wrong:vim=15'})
conf.init_settings(Mock()) settings.init()
assert settings.rules == ['bash', 'lisp'] assert settings.rules == ['bash', 'lisp']
assert settings.exclude_rules == ['git', 'vim'] assert settings.exclude_rules == ['git', 'vim']
assert settings.wait_command == 55 assert settings.wait_command == 55
@ -79,26 +70,26 @@ class TestSettingsFromEnv(object):
def test_from_env_with_DEFAULT(self, environ, settings): def test_from_env_with_DEFAULT(self, environ, settings):
environ.update({'THEFUCK_RULES': 'DEFAULT_RULES:bash:lisp'}) environ.update({'THEFUCK_RULES': 'DEFAULT_RULES:bash:lisp'})
conf.init_settings(Mock()) settings.init()
assert settings.rules == conf.DEFAULT_RULES + ['bash', 'lisp'] assert settings.rules == conf.DEFAULT_RULES + ['bash', 'lisp']
class TestInitializeSettingsFile(object): class TestInitializeSettingsFile(object):
def test_ignore_if_exists(self): def test_ignore_if_exists(self, settings):
settings_path_mock = Mock(is_file=Mock(return_value=True), open=Mock()) settings_path_mock = Mock(is_file=Mock(return_value=True), open=Mock())
user_dir_mock = Mock(joinpath=Mock(return_value=settings_path_mock)) settings.user_dir = Mock(joinpath=Mock(return_value=settings_path_mock))
conf.initialize_settings_file(user_dir_mock) settings._init_settings_file()
assert settings_path_mock.is_file.call_count == 1 assert settings_path_mock.is_file.call_count == 1
assert not settings_path_mock.open.called assert not settings_path_mock.open.called
def test_create_if_doesnt_exists(self): def test_create_if_doesnt_exists(self, settings):
settings_file = six.StringIO() settings_file = six.StringIO()
settings_path_mock = Mock( settings_path_mock = Mock(
is_file=Mock(return_value=False), is_file=Mock(return_value=False),
open=Mock(return_value=Mock( open=Mock(return_value=Mock(
__exit__=lambda *args: None, __enter__=lambda *args: settings_file))) __exit__=lambda *args: None, __enter__=lambda *args: settings_file)))
user_dir_mock = Mock(joinpath=Mock(return_value=settings_path_mock)) settings.user_dir = Mock(joinpath=Mock(return_value=settings_path_mock))
conf.initialize_settings_file(user_dir_mock) settings._init_settings_file()
settings_file_contents = settings_file.getvalue() settings_file_contents = settings_file.getvalue()
assert settings_path_mock.is_file.call_count == 1 assert settings_path_mock.is_file.call_count == 1
assert settings_path_mock.open.call_count == 1 assert settings_path_mock.open.call_count == 1

View File

@ -1,24 +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, 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 from thefuck.corrector import get_corrected_commands, 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')
class TestGetRules(object): class TestGetRules(object):
@ -31,18 +15,12 @@ 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):
assert {r.name for r in rules} == set(names) assert {r.name for r in rules} == set(names)
def _prepare_rules(self, rules):
if rules == conf.DEFAULT_RULES:
return rules
else:
return types.RulesNamesList(rules)
@pytest.mark.parametrize('paths, conf_rules, exclude_rules, loaded_rules', [ @pytest.mark.parametrize('paths, conf_rules, exclude_rules, loaded_rules', [
(['git.py', 'bash.py'], conf.DEFAULT_RULES, [], ['git', 'bash']), (['git.py', 'bash.py'], conf.DEFAULT_RULES, [], ['git', 'bash']),
(['git.py', 'bash.py'], ['git'], [], ['git']), (['git.py', 'bash.py'], ['git'], [], ['git']),
@ -51,44 +29,13 @@ class TestGetRules(object):
def test_get_rules(self, glob, settings, paths, conf_rules, exclude_rules, def test_get_rules(self, glob, settings, paths, conf_rules, exclude_rules,
loaded_rules): loaded_rules):
glob([PosixPath(path) for path in paths]) glob([PosixPath(path) for path in paths])
settings.update(rules=self._prepare_rules(conf_rules), settings.update(rules=conf_rules,
priority={}, priority={},
exclude_rules=self._prepare_rules(exclude_rules)) exclude_rules=exclude_rules)
rules = corrector.get_rules() rules = corrector.get_rules()
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),
@ -100,3 +47,13 @@ def test_get_corrected_commands(mocker):
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)] \ assert [cmd.script for cmd in get_corrected_commands(command)] \
== ['test!', 'test@', 'test;'] == ['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)]

View File

@ -1,46 +0,0 @@
import pytest
from subprocess import PIPE
from mock import Mock
from thefuck import main
from tests.utils import Command
class TestGetCommand(object):
@pytest.fixture(autouse=True)
def Popen(self, monkeypatch):
Popen = Mock()
Popen.return_value.stdout.read.return_value = b'stdout'
Popen.return_value.stderr.read.return_value = b'stderr'
monkeypatch.setattr('thefuck.main.Popen', Popen)
return Popen
@pytest.fixture(autouse=True)
def prepare(self, monkeypatch):
monkeypatch.setattr('thefuck.main.os.environ', {})
monkeypatch.setattr('thefuck.main.wait_output', lambda *_: True)
@pytest.fixture(autouse=True)
def generic_shell(self, monkeypatch):
monkeypatch.setattr('thefuck.shells.from_shell', lambda x: x)
monkeypatch.setattr('thefuck.shells.to_shell', lambda x: x)
def test_get_command_calls(self, Popen, settings):
settings.env = {}
assert main.get_command(['thefuck', 'apt-get', 'search', 'vim']) \
== Command('apt-get search vim', 'stdout', 'stderr')
Popen.assert_called_once_with('apt-get search vim',
shell=True,
stdout=PIPE,
stderr=PIPE,
env={})
@pytest.mark.parametrize('args, result', [
(['thefuck', ''], None),
(['thefuck', '', ''], None),
(['thefuck', 'ls', '-la'], 'ls -la'),
(['thefuck', 'ls'], 'ls')])
def test_get_command_script(self, args, result):
if result:
assert main.get_command(args).script == result
else:
assert main.get_command(args) is None

View File

@ -1,14 +1,10 @@
from tests.utils import root def test_readme(source_root):
with source_root.joinpath('README.md').open() as f:
def test_readme():
with root.joinpath('README.md').open() as f:
readme = f.read() readme = f.read()
bundled = root \ bundled = source_root.joinpath('thefuck') \
.joinpath('thefuck') \ .joinpath('rules') \
.joinpath('rules') \ .glob('*.py')
.glob('*.py')
for rule in bundled: for rule in bundled:
if rule.stem != '__init__': if rule.stem != '__init__':

View File

@ -1,43 +1,10 @@
from thefuck.types import RulesNamesList, Settings, \ from subprocess import PIPE
SortedCorrectedCommandsSequence from mock import Mock
from tests.utils import Rule, CorrectedCommand from pathlib import Path
import pytest
from tests.utils import CorrectedCommand, Rule, Command
def test_rules_names_list(): from thefuck import conf
assert RulesNamesList(['bash', 'lisp']) == ['bash', 'lisp'] from thefuck.exceptions import EmptyCommand
assert RulesNamesList(['bash', 'lisp']) == RulesNamesList(['bash', 'lisp'])
assert Rule('lisp') in RulesNamesList(['lisp'])
assert Rule('bash') not in RulesNamesList(['lisp'])
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): class TestCorrectedCommand(object):
@ -51,3 +18,108 @@ 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)]
class TestCommand(object):
@pytest.fixture(autouse=True)
def Popen(self, monkeypatch):
Popen = Mock()
Popen.return_value.stdout.read.return_value = b'stdout'
Popen.return_value.stderr.read.return_value = b'stderr'
monkeypatch.setattr('thefuck.types.Popen', Popen)
return Popen
@pytest.fixture(autouse=True)
def prepare(self, monkeypatch):
monkeypatch.setattr('thefuck.types.os.environ', {})
monkeypatch.setattr('thefuck.types.Command._wait_output',
staticmethod(lambda *_: True))
@pytest.fixture(autouse=True)
def generic_shell(self, monkeypatch):
monkeypatch.setattr('thefuck.shells.from_shell', lambda x: x)
monkeypatch.setattr('thefuck.shells.to_shell', lambda x: x)
def test_from_script_calls(self, Popen, settings):
settings.env = {}
assert Command.from_raw_script(
['apt-get', 'search', 'vim']) == Command(
'apt-get search vim', 'stdout', 'stderr')
Popen.assert_called_once_with('apt-get search vim',
shell=True,
stdout=PIPE,
stderr=PIPE,
env={})
@pytest.mark.parametrize('script, result', [
([''], None),
(['', ''], None),
(['ls', '-la'], 'ls -la'),
(['ls'], 'ls')])
def test_from_script(self, script, result):
if result:
assert Command.from_raw_script(script).script == result
else:
with pytest.raises(EmptyCommand):
Command.from_raw_script(script)

View File

@ -1,10 +1,9 @@
# -*- encoding: utf-8 -*- # -*- encoding: utf-8 -*-
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, SortedCorrectedCommandsSequence from thefuck.types import CorrectedCommand
@pytest.fixture @pytest.fixture
@ -41,10 +40,8 @@ def test_read_actions(patch_getch):
def test_command_selector(): def test_command_selector():
selector = ui.CommandSelector([1, 2, 3]) selector = ui.CommandSelector(iter([1, 2, 3]))
assert selector.value == 1 assert selector.value == 1
changes = []
selector.on_change(changes.append)
selector.next() selector.next()
assert selector.value == 2 assert selector.value == 2
selector.next() selector.next()
@ -53,58 +50,55 @@ def test_command_selector():
assert selector.value == 1 assert selector.value == 1
selector.previous() selector.previous()
assert selector.value == 3 assert selector.value == 3
assert changes == [1, 2, 3, 1, 3]
@pytest.mark.usefixtures('no_colors') @pytest.mark.usefixtures('no_colors')
class TestSelectCommand(object): class TestSelectCommand(object):
@pytest.fixture @pytest.fixture
def commands_with_side_effect(self): def commands_with_side_effect(self):
return SortedCorrectedCommandsSequence( return [CorrectedCommand('ls', lambda *_: None, 100),
iter([CorrectedCommand('ls', lambda *_: None, 100), CorrectedCommand('cd', lambda *_: None, 100)]
CorrectedCommand('cd', lambda *_: None, 100)]))
@pytest.fixture @pytest.fixture
def commands(self): def commands(self):
return SortedCorrectedCommandsSequence( return [CorrectedCommand('ls', None, 100),
iter([CorrectedCommand('ls', None, 100), CorrectedCommand('cd', None, 100)]
CorrectedCommand('cd', None, 100)]))
def test_without_commands(self, capsys): 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') assert capsys.readouterr() == ('', 'No fucks given\n')
def test_without_confirmation(self, capsys, commands, settings): def test_without_confirmation(self, capsys, commands, settings):
settings.require_confirmation = False settings.require_confirmation = False
assert ui.select_command(commands) == commands[0] assert ui.select_command(iter(commands)) == commands[0]
assert capsys.readouterr() == ('', 'ls\n') assert capsys.readouterr() == ('', 'ls\n')
def test_without_confirmation_with_side_effects( def test_without_confirmation_with_side_effects(
self, capsys, commands_with_side_effect, settings): self, capsys, commands_with_side_effect, settings):
settings.require_confirmation = False 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] == commands_with_side_effect[0]
assert capsys.readouterr() == ('', 'ls (+side effect)\n') assert capsys.readouterr() == ('', 'ls (+side effect)\n')
def test_with_confirmation(self, capsys, patch_getch, commands): def test_with_confirmation(self, capsys, patch_getch, commands):
patch_getch(['\n']) 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') assert capsys.readouterr() == ('', u'\x1b[1K\rls [enter/↑/↓/ctrl+c]\n')
def test_with_confirmation_abort(self, capsys, patch_getch, commands): def test_with_confirmation_abort(self, capsys, patch_getch, commands):
patch_getch([KeyboardInterrupt]) 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') assert capsys.readouterr() == ('', u'\x1b[1K\rls [enter/↑/↓/ctrl+c]\nAborted\n')
def test_with_confirmation_with_side_effct(self, capsys, patch_getch, def test_with_confirmation_with_side_effct(self, capsys, patch_getch,
commands_with_side_effect): commands_with_side_effect):
patch_getch(['\n']) 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] == commands_with_side_effect[0]
assert capsys.readouterr() == ('', u'\x1b[1K\rls (+side effect) [enter/↑/↓/ctrl+c]\n') assert capsys.readouterr() == ('', u'\x1b[1K\rls (+side effect) [enter/↑/↓/ctrl+c]\n')
def test_with_confirmation_select_second(self, capsys, patch_getch, commands): def test_with_confirmation_select_second(self, capsys, patch_getch, commands):
patch_getch(['\x1b', '[', 'B', '\n']) patch_getch(['\x1b', '[', 'B', '\n'])
assert ui.select_command(commands) == commands[1] assert ui.select_command(iter(commands)) == commands[1]
assert capsys.readouterr() == ( assert capsys.readouterr() == (
'', u'\x1b[1K\rls [enter/↑/↓/ctrl+c]\x1b[1K\rcd [enter/↑/↓/ctrl+c]\n') '', u'\x1b[1K\rls [enter/↑/↓/ctrl+c]\x1b[1K\rcd [enter/↑/↓/ctrl+c]\n')

View File

@ -1,25 +1,25 @@
from pathlib import Path
from thefuck import types from thefuck import types
from thefuck.conf import DEFAULT_PRIORITY from thefuck.conf import DEFAULT_PRIORITY
def Command(script='', stdout='', stderr=''): class Command(types.Command):
return types.Command(script, stdout, stderr) def __init__(self, script='', stdout='', stderr=''):
super(Command, self).__init__(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()

View File

@ -1,26 +1,12 @@
from imp import load_source from imp import load_source
import os import os
import sys import sys
from pathlib import Path
from six import text_type from six import text_type
from .types import RulesNamesList, Settings
class _DefaultRulesNames(RulesNamesList): ALL_ENABLED = object()
def __add__(self, items): DEFAULT_RULES = [ALL_ENABLED]
return _DefaultRulesNames(list(self) + items)
def __contains__(self, item):
return item.enabled_by_default or \
super(_DefaultRulesNames, self).__contains__(item)
def __eq__(self, other):
if isinstance(other, _DefaultRulesNames):
return super(_DefaultRulesNames, self).__eq__(other)
else:
return False
DEFAULT_RULES = _DefaultRulesNames([])
DEFAULT_PRIORITY = 1000 DEFAULT_PRIORITY = 1000
DEFAULT_SETTINGS = {'rules': DEFAULT_RULES, DEFAULT_SETTINGS = {'rules': DEFAULT_RULES,
@ -53,85 +39,89 @@ SETTINGS_HEADER = u"""# ~/.thefuck/settings.py: The Fuck settings file
""" """
def _settings_from_file(user_dir): class Settings(dict):
"""Loads settings from file.""" def __getattr__(self, item):
settings = load_source('settings', return self.get(item)
text_type(user_dir.joinpath('settings.py')))
return {key: getattr(settings, key)
for key in DEFAULT_SETTINGS.keys()
if hasattr(settings, key)}
def __setattr__(self, key, value):
self[key] = value
def _rules_from_env(val): def init(self):
"""Transforms rules list from env-string to python.""" """Fills `settings` with values from `settings.py` and env."""
val = val.split(':') from .logs import exception
if 'DEFAULT_RULES' in val:
val = DEFAULT_RULES + [rule for rule in val if rule != 'DEFAULT_RULES']
return val
self._setup_user_dir()
self._init_settings_file()
def _priority_from_env(val):
"""Gets priority pairs from env."""
for part in val.split(':'):
try: try:
rule, priority = part.split('=') self.update(self._settings_from_file())
yield rule, int(priority) except Exception:
except ValueError: exception("Can't load settings from file", sys.exc_info())
continue
try:
self.update(self._settings_from_env())
except Exception:
exception("Can't load settings from env", sys.exc_info())
def _val_from_env(env, attr): def _init_settings_file(self):
"""Transforms env-strings to python.""" settings_path = self.user_dir.joinpath('settings.py')
val = os.environ[env] if not settings_path.is_file():
if attr in ('rules', 'exclude_rules'): with settings_path.open(mode='w') as settings_file:
return _rules_from_env(val) settings_file.write(SETTINGS_HEADER)
elif attr == 'priority': for setting in DEFAULT_SETTINGS.items():
return dict(_priority_from_env(val)) settings_file.write(u'# {} = {}\n'.format(*setting))
elif attr == 'wait_command':
return int(val) def _setup_user_dir(self):
elif attr in ('require_confirmation', 'no_colors', 'debug'): """Returns user config dir, create it when it doesn't exist."""
return val.lower() == 'true' user_dir = Path(os.path.expanduser('~/.thefuck'))
else: rules_dir = user_dir.joinpath('rules')
if not rules_dir.is_dir():
rules_dir.mkdir(parents=True)
self.user_dir = user_dir
def _settings_from_file(self):
"""Loads settings from file."""
settings = load_source(
'settings', text_type(self.user_dir.joinpath('settings.py')))
return {key: getattr(settings, key)
for key in DEFAULT_SETTINGS.keys()
if hasattr(settings, key)}
def _rules_from_env(self, val):
"""Transforms rules list from env-string to python."""
val = val.split(':')
if 'DEFAULT_RULES' in val:
val = DEFAULT_RULES + [rule for rule in val if rule != 'DEFAULT_RULES']
return val return val
def _priority_from_env(self, val):
"""Gets priority pairs from env."""
for part in val.split(':'):
try:
rule, priority = part.split('=')
yield rule, int(priority)
except ValueError:
continue
def _settings_from_env(): def _val_from_env(self, env, attr):
"""Loads settings from env.""" """Transforms env-strings to python."""
return {attr: _val_from_env(env, attr) val = os.environ[env]
for env, attr in ENV_TO_ATTR.items() if attr in ('rules', 'exclude_rules'):
if env in os.environ} return self._rules_from_env(val)
elif attr == 'priority':
return dict(self._priority_from_env(val))
elif attr == 'wait_command':
return int(val)
elif attr in ('require_confirmation', 'no_colors', 'debug'):
return val.lower() == 'true'
else:
return val
def _settings_from_env(self):
"""Loads settings from env."""
return {attr: self._val_from_env(env, attr)
for env, attr in ENV_TO_ATTR.items()
if env in os.environ}
settings = Settings(DEFAULT_SETTINGS) settings = Settings(DEFAULT_SETTINGS)
def init_settings(user_dir):
"""Fills `settings` with values from `settings.py` and env."""
from .logs import exception
settings.user_dir = user_dir
try:
settings.update(_settings_from_file(user_dir))
except Exception:
exception("Can't load settings from file", sys.exc_info())
try:
settings.update(_settings_from_env())
except Exception:
exception("Can't load settings from env", sys.exc_info())
if not isinstance(settings['rules'], RulesNamesList):
settings.rules = RulesNamesList(settings['rules'])
if not isinstance(settings.exclude_rules, RulesNamesList):
settings.exclude_rules = RulesNamesList(settings.exclude_rules)
def initialize_settings_file(user_dir):
settings_path = user_dir.joinpath('settings.py')
if not settings_path.is_file():
with settings_path.open(mode='w') as settings_file:
settings_file.write(SETTINGS_HEADER)
for setting in DEFAULT_SETTINGS.items():
settings_file.write(u'# {} = {}\n'.format(*setting))

View File

@ -1,38 +1,29 @@
import sys
from imp import load_source
from pathlib import Path from pathlib import Path
from .conf import settings, DEFAULT_PRIORITY from .conf import settings
from .types import Rule, CorrectedCommand, SortedCorrectedCommandsSequence 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.""" """Yields all available rules.
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))
:type rules_paths: [Path]
:rtype: Iterable[Rule]
def get_loaded_rules(rules): """
"""Yields all available rules.""" for path in rules_paths:
for rule in rules: if path.name != '__init__.py':
if rule.name != '__init__.py': rule = Rule.from_path(path)
loaded_rule = load_rule(rule) if rule.is_enabled:
if loaded_rule in settings.rules and \ yield rule
loaded_rule not in settings.exclude_rules:
yield loaded_rule
def get_rules(): def get_rules():
"""Returns all enabled rules.""" """Returns all enabled rules.
:rtype: [Rule]
"""
bundled = Path(__file__).parent \ bundled = Path(__file__).parent \
.joinpath('rules') \ .joinpath('rules') \
.glob('*.py') .glob('*.py')
@ -41,34 +32,44 @@ def get_rules():
key=lambda rule: rule.priority) key=lambda rule: rule.priority)
def is_rule_match(command, rule): def organize_commands(corrected_commands):
"""Returns first matched rule for command.""" """Yields sorted commands without duplicates.
script_only = command.stdout is None and command.stderr is None
if script_only and rule.requires_output: :type corrected_commands: Iterable[thefuck.types.CorrectedCommand]
return False :rtype: Iterable[thefuck.types.CorrectedCommand]
"""
try: try:
with logs.debug_time(u'Trying rule: {};'.format(rule.name)): first_command = next(corrected_commands)
if compatibility_call(rule.match, command): yield first_command
return True except StopIteration:
except Exception: return
logs.rule_failed(rule, sys.exc_info())
without_duplicates = {
command for command in sorted(
corrected_commands, key=lambda command: command.priority)
if command != first_command}
def make_corrected_commands(command, rule): sorted_commands = sorted(
new_commands = compatibility_call(rule.get_new_command, command) without_duplicates,
if not isinstance(new_commands, list): key=lambda corrected_command: corrected_command.priority)
new_commands = (new_commands,)
for n, new_command in enumerate(new_commands): logs.debug('Corrected commands: '.format(
yield CorrectedCommand(script=new_command, ', '.join(str(cmd) for cmd in [first_command] + sorted_commands)))
side_effect=rule.side_effect,
priority=(n + 1) * rule.priority) for command in sorted_commands:
yield command
def get_corrected_commands(command): def get_corrected_commands(command):
"""Returns generator with sorted and unique corrected commands.
:type command: thefuck.types.Command
:rtype: Iterable[thefuck.types.CorrectedCommand]
"""
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 SortedCorrectedCommandsSequence(corrected_commands) return organize_commands(corrected_commands)

6
thefuck/exceptions.py Normal file
View File

@ -0,0 +1,6 @@
class EmptyCommand(Exception):
"""Raised when empty command passed to `thefuck`."""
class NoRuleMatched(Exception):
"""Raised when no rule matched for some command."""

View File

@ -1,114 +1,38 @@
from argparse import ArgumentParser from argparse import ArgumentParser
from warnings import warn from warnings import warn
from pathlib import Path
from os.path import expanduser
from pprint import pformat from pprint import pformat
import pkg_resources
from subprocess import Popen, PIPE
import os
import sys import sys
from psutil import Process, TimeoutExpired
import colorama import colorama
import six
from . import logs, types, shells from . import logs, types, shells
from .conf import initialize_settings_file, init_settings, settings from .conf import settings
from .corrector import get_corrected_commands from .corrector import get_corrected_commands
from .utils import compatibility_call from .exceptions import EmptyCommand
from .utils import get_installation_info
from .ui import select_command from .ui import select_command
def setup_user_dir():
"""Returns user config dir, create it when it doesn't exist."""
user_dir = Path(expanduser('~/.thefuck'))
rules_dir = user_dir.joinpath('rules')
if not rules_dir.is_dir():
rules_dir.mkdir(parents=True)
initialize_settings_file(user_dir)
return user_dir
def wait_output(popen):
"""Returns `True` if we can get output of the command in the
`settings.wait_command` time.
Command will be killed if it wasn't finished in the time.
"""
proc = Process(popen.pid)
try:
proc.wait(settings.wait_command)
return True
except TimeoutExpired:
for child in proc.children(recursive=True):
child.kill()
proc.kill()
return False
def get_command(args):
"""Creates command from `args` and executes it."""
if six.PY2:
script = ' '.join(arg.decode('utf-8') for arg in args[1:])
else:
script = ' '.join(args[1:])
script = script.strip()
if not script:
return
script = shells.from_shell(script)
env = dict(os.environ)
env.update(settings.env)
with logs.debug_time(u'Call: {}; with env: {};'.format(script, env)):
result = Popen(script, shell=True, stdout=PIPE, stderr=PIPE, env=env)
if wait_output(result):
stdout = result.stdout.read().decode('utf-8')
stderr = result.stderr.read().decode('utf-8')
logs.debug(u'Received stdout: {}'.format(stdout))
logs.debug(u'Received stderr: {}'.format(stderr))
return types.Command(script, stdout, stderr)
else:
logs.debug(u'Execution timed out!')
return types.Command(script, None, None)
def run_command(old_cmd, command):
"""Runs command from rule for passed command."""
if command.side_effect:
compatibility_call(command.side_effect, old_cmd, command.script)
shells.put_to_history(command.script)
print(command.script)
# Entry points:
def fix_command(): def fix_command():
"""Fixes previous command. Used when `thefuck` called without arguments."""
colorama.init() colorama.init()
user_dir = setup_user_dir() settings.init()
init_settings(user_dir)
with logs.debug_time('Total'): with logs.debug_time('Total'):
logs.debug(u'Run with settings: {}'.format(pformat(settings))) logs.debug(u'Run with settings: {}'.format(pformat(settings)))
command = get_command(sys.argv) try:
command = types.Command.from_raw_script(sys.argv[1:])
if not command: except EmptyCommand:
logs.debug('Empty command, nothing to do') logs.debug('Empty command, nothing to do')
return return
corrected_commands = get_corrected_commands(command) corrected_commands = get_corrected_commands(command)
selected_command = select_command(corrected_commands) selected_command = select_command(corrected_commands)
if selected_command: if selected_command:
run_command(command, selected_command) selected_command.run(command)
def _get_current_version():
return pkg_resources.require('thefuck')[0].version
def print_alias(entry_point=True): def print_alias(entry_point=True):
"""Prints alias for current shell."""
if entry_point: if entry_point:
warn('`thefuck-alias` is deprecated, use `thefuck --alias` instead.') warn('`thefuck-alias` is deprecated, use `thefuck --alias` instead.')
position = 1 position = 1
@ -128,16 +52,16 @@ def how_to_configure_alias():
""" """
colorama.init() colorama.init()
user_dir = setup_user_dir() settings.init()
init_settings(user_dir)
logs.how_to_configure_alias(shells.how_to_configure()) logs.how_to_configure_alias(shells.how_to_configure())
def main(): def main():
parser = ArgumentParser(prog='thefuck') parser = ArgumentParser(prog='thefuck')
version = get_installation_info().version
parser.add_argument('-v', '--version', parser.add_argument('-v', '--version',
action='version', action='version',
version='%(prog)s {}'.format(_get_current_version())) version='%(prog)s {}'.format(version))
parser.add_argument('-a', '--alias', parser.add_argument('-a', '--alias',
action='store_true', action='store_true',
help='[custom-alias-name] prints alias for current shell') help='[custom-alias-name] prints alias for current shell')

View File

@ -27,6 +27,6 @@ def git_support(fn, command):
expansion = ' '.join(map(quote, split(search.group(2)))) expansion = ' '.join(map(quote, split(search.group(2))))
new_script = command.script.replace(alias, expansion) new_script = command.script.replace(alias, expansion)
command = Command._replace(command, script=new_script) command = command.update(script=new_script)
return fn(command) return fn(command)

View File

@ -9,9 +9,7 @@ def sudo_support(fn, command):
if not command.script.startswith('sudo '): if not command.script.startswith('sudo '):
return fn(command) return fn(command)
result = fn(Command(command.script[5:], result = fn(command.update(script=command.script[5:]))
command.stdout,
command.stderr))
if result and isinstance(result, six.string_types): if result and isinstance(result, six.string_types):
return u'sudo {}'.format(result) return u'sudo {}'.format(result)

View File

@ -1,15 +1,245 @@
from collections import namedtuple from imp import load_source
from traceback import format_stack import os
from subprocess import Popen, PIPE
import sys
from psutil import Process, TimeoutExpired
import six
from .conf import settings, DEFAULT_PRIORITY, ALL_ENABLED
from .utils import compatibility_call
from .exceptions import EmptyCommand
from . import logs, shells
Command = namedtuple('Command', ('script', 'stdout', 'stderr'))
Rule = namedtuple('Rule', ('name', 'match', 'get_new_command', class Command(object):
'enabled_by_default', 'side_effect', """Command that should be fixed."""
'priority', 'requires_output'))
def __init__(self, script, stdout, stderr):
"""Initializes command with given values.
:type script: basestring
:type stdout: basestring
:type stderr: basestring
"""
self.script = script
self.stdout = stdout
self.stderr = stderr
def __eq__(self, other):
if isinstance(other, Command):
return (self.script, self.stdout, self.stderr) \
== (other.script, other.stdout, other.stderr)
else:
return False
def __repr__(self):
return 'Command(script={}, stdout={}, stderr={})'.format(
self.script, self.stdout, self.stderr)
def update(self, **kwargs):
"""Returns new command with replaced fields.
:rtype: Command
"""
kwargs.setdefault('script', self.script)
kwargs.setdefault('stdout', self.stdout)
kwargs.setdefault('stderr', self.stderr)
return Command(**kwargs)
@staticmethod
def _wait_output(popen):
"""Returns `True` if we can get output of the command in the
`settings.wait_command` time.
Command will be killed if it wasn't finished in the time.
:type popen: Popen
:rtype: bool
"""
proc = Process(popen.pid)
try:
proc.wait(settings.wait_command)
return True
except TimeoutExpired:
for child in proc.children(recursive=True):
child.kill()
proc.kill()
return False
@staticmethod
def _prepare_script(raw_script):
"""Creates single script from a list of script parts.
:type raw_script: [basestring]
:rtype: basestring
"""
if six.PY2:
script = ' '.join(arg.decode('utf-8') for arg in raw_script)
else:
script = ' '.join(raw_script)
script = script.strip()
return shells.from_shell(script)
@classmethod
def from_raw_script(cls, raw_script):
"""Creates instance of `Command` from a list of script parts.
:type raw_script: [basestring]
:rtype: Command
:raises: EmptyCommand
"""
script = cls._prepare_script(raw_script)
if not script:
raise EmptyCommand
env = dict(os.environ)
env.update(settings.env)
with logs.debug_time(u'Call: {}; with env: {};'.format(script, env)):
result = Popen(script, shell=True, stdout=PIPE, stderr=PIPE, env=env)
if cls._wait_output(result):
stdout = result.stdout.read().decode('utf-8')
stderr = result.stderr.read().decode('utf-8')
logs.debug(u'Received stdout: {}'.format(stdout))
logs.debug(u'Received stderr: {}'.format(stderr))
return cls(script, stdout, stderr)
else:
logs.debug(u'Execution timed out!')
return cls(script, None, None)
class Rule(object):
"""Rule for fixing commands."""
def __init__(self, name, match, get_new_command,
enabled_by_default, side_effect,
priority, requires_output):
"""Initializes rule with given fields.
:type name: basestring
:type match: (Command) -> bool
:type get_new_command: (Command) -> (basestring | [basestring])
:type enabled_by_default: boolean
:type side_effect: (Command, basestring) -> None
:type priority: int
:type requires_output: bool
"""
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.
:type path: pathlib.Path
:rtype: Rule
"""
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):
"""Returns `True` when rule enabled.
:rtype: bool
"""
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.
:type command: Command
:rtype: bool
"""
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):
"""Returns generator with corrected commands.
:type command: Command
:rtype: Iterable[CorrectedCommand]
"""
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):
"""Corrected by rule command."""
def __init__(self, script, side_effect, priority): def __init__(self, script, side_effect, priority):
"""Initializes instance with given fields.
:type script: basestring
:type side_effect: (Command, basestring) -> None
:type priority: int
"""
self.script = script self.script = script
self.side_effect = side_effect self.side_effect = side_effect
self.priority = priority self.priority = priority
@ -28,77 +258,14 @@ 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)
def run(self, old_cmd):
"""Runs command from rule for passed command.
:type old_cmd: Command
class RulesNamesList(list): """
"""Wrapper a top of list for storing rules names.""" if self.side_effect:
compatibility_call(self.side_effect, old_cmd, self.script)
def __contains__(self, item): shells.put_to_history(self.script)
return super(RulesNamesList, self).__contains__(item.name) print(self.script)
class Settings(dict):
def __getattr__(self, item):
return self.get(item)
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)

View File

@ -2,6 +2,7 @@
import sys import sys
from .conf import settings from .conf import settings
from .exceptions import NoRuleMatched
from . import logs from . import logs
try: try:
@ -50,27 +51,36 @@ def read_actions():
class CommandSelector(object): class CommandSelector(object):
"""Helper for selecting rule from rules list."""
def __init__(self, commands): def __init__(self, commands):
self._commands = commands """:type commands: Iterable[thefuck.types.CorrectedCommand]"""
self._commands_gen = commands
try:
self._commands = [next(self._commands_gen)]
except StopIteration:
raise NoRuleMatched
self._realised = False
self._index = 0 self._index = 0
self._on_change = lambda x: x
def _realise(self):
if not self._realised:
self._commands += list(self._commands_gen)
self._realised = True
def next(self): def next(self):
self._realise()
self._index = (self._index + 1) % len(self._commands) self._index = (self._index + 1) % len(self._commands)
self._on_change(self.value)
def previous(self): def previous(self):
self._realise()
self._index = (self._index - 1) % len(self._commands) self._index = (self._index - 1) % len(self._commands)
self._on_change(self.value)
@property @property
def value(self): def value(self):
""":rtype hefuck.types.CorrectedCommand"""
return self._commands[self._index] return self._commands[self._index]
def on_change(self, fn):
self._on_change = fn
fn(self.value)
def select_command(corrected_commands): def select_command(corrected_commands):
"""Returns: """Returns:
@ -79,17 +89,22 @@ def select_command(corrected_commands):
- None when ctrl+c pressed; - None when ctrl+c pressed;
- selected command. - selected command.
:type corrected_commands: Iterable[thefuck.types.CorrectedCommand]
:rtype: thefuck.types.CorrectedCommand | None
""" """
if not corrected_commands: try:
selector = CommandSelector(corrected_commands)
except NoRuleMatched:
logs.failed('No fucks given') logs.failed('No fucks given')
return return
selector = CommandSelector(corrected_commands)
if not settings.require_confirmation: if not settings.require_confirmation:
logs.show_corrected_command(selector.value) logs.show_corrected_command(selector.value)
return selector.value return selector.value
selector.on_change(lambda val: logs.confirm_text(val)) logs.confirm_text(selector.value)
for action in read_actions(): for action in read_actions():
if action == SELECT: if action == SELECT:
sys.stderr.write('\n') sys.stderr.write('\n')
@ -99,5 +114,7 @@ def select_command(corrected_commands):
return return
elif action == PREVIOUS: elif action == PREVIOUS:
selector.previous() selector.previous()
logs.confirm_text(selector.value)
elif action == NEXT: elif action == NEXT:
selector.next() selector.next()
logs.confirm_text(selector.value)

View File

@ -4,7 +4,6 @@ import shelve
from warnings import warn from warnings import warn
from decorator import decorator from decorator import decorator
from contextlib import closing from contextlib import closing
import tempfile
import os import os
import pickle import pickle
@ -99,10 +98,9 @@ def get_all_executables():
return fallback return fallback
tf_alias = thefuck_alias() tf_alias = thefuck_alias()
tf_entry_points = pkg_resources.require('thefuck')[0]\ tf_entry_points = get_installation_info().get_entry_map()\
.get_entry_map()\ .get('console_scripts', {})\
.get('console_scripts', {})\ .keys()
.keys()
bins = [exe.name bins = [exe.name
for path in os.environ.get('PATH', '').split(':') for path in os.environ.get('PATH', '').split(':')
for exe in _safe(lambda: list(Path(path).iterdir()), []) for exe in _safe(lambda: list(Path(path).iterdir()), [])
@ -224,3 +222,7 @@ def compatibility_call(fn, *args):
.format(fn.__name__, fn.__module__)) .format(fn.__name__, fn.__module__))
args += (settings,) args += (settings,)
return fn(*args) return fn(*args)
def get_installation_info():
return pkg_resources.require('thefuck')[0]