diff --git a/tests/test_conf.py b/tests/test_conf.py index 2137f7c7..411cd063 100644 --- a/tests/test_conf.py +++ b/tests/test_conf.py @@ -29,7 +29,7 @@ def environ(monkeypatch): def test_settings_defaults(load_source): load_source.return_value = object() for key, val in conf.DEFAULT_SETTINGS.items(): - assert getattr(conf.get_settings(Mock()), key) == val + assert getattr(conf.init_settings(Mock()), key) == val @pytest.mark.usefixture('environ') @@ -41,7 +41,7 @@ class TestSettingsFromFile(object): no_colors=True, priority={'vim': 100}, exclude_rules=['git']) - settings = conf.get_settings(Mock()) + settings = conf.init_settings(Mock()) assert settings.rules == ['test'] assert settings.wait_command == 10 assert settings.require_confirmation is True @@ -55,7 +55,7 @@ class TestSettingsFromFile(object): exclude_rules=[], require_confirmation=True, no_colors=True) - settings = conf.get_settings(Mock()) + settings = conf.init_settings(Mock()) assert settings.rules == conf.DEFAULT_RULES + ['test'] @@ -68,7 +68,7 @@ class TestSettingsFromEnv(object): 'THEFUCK_REQUIRE_CONFIRMATION': 'true', 'THEFUCK_NO_COLORS': 'false', 'THEFUCK_PRIORITY': 'bash=10:lisp=wrong:vim=15'}) - settings = conf.get_settings(Mock()) + settings = conf.init_settings(Mock()) assert settings.rules == ['bash', 'lisp'] assert settings.exclude_rules == ['git', 'vim'] assert settings.wait_command == 55 @@ -78,7 +78,7 @@ class TestSettingsFromEnv(object): def test_from_env_with_DEFAULT(self, environ): environ.update({'THEFUCK_RULES': 'DEFAULT_RULES:bash:lisp'}) - settings = conf.get_settings(Mock()) + settings = conf.init_settings(Mock()) assert settings.rules == conf.DEFAULT_RULES + ['bash', 'lisp'] diff --git a/thefuck/conf.py b/thefuck/conf.py index e107329a..2b8178ba 100644 --- a/thefuck/conf.py +++ b/thefuck/conf.py @@ -1,12 +1,11 @@ -from copy import copy from imp import load_source import os import sys from six import text_type -from . import logs, types +from .types import RulesNamesList, Settings -class _DefaultRulesNames(types.RulesNamesList): +class _DefaultRulesNames(RulesNamesList): def __add__(self, items): return _DefaultRulesNames(list(self) + items) @@ -24,7 +23,6 @@ class _DefaultRulesNames(types.RulesNamesList): DEFAULT_RULES = _DefaultRulesNames([]) DEFAULT_PRIORITY = 1000 - DEFAULT_SETTINGS = {'rules': DEFAULT_RULES, 'exclude_rules': [], 'wait_command': 3, @@ -42,7 +40,6 @@ ENV_TO_ATTR = {'THEFUCK_RULES': 'rules', 'THEFUCK_PRIORITY': 'priority', 'THEFUCK_DEBUG': 'debug'} - SETTINGS_HEADER = u"""# ~/.thefuck/settings.py: The Fuck settings file # # The rules are defined as in the example bellow: @@ -105,30 +102,28 @@ def _settings_from_env(): if env in os.environ} -def get_settings(user_dir): - """Returns settings filled with values from `settings.py` and env.""" - conf = copy(DEFAULT_SETTINGS) - try: - conf.update(_settings_from_file(user_dir)) - except Exception: - logs.exception("Can't load settings from file", - sys.exc_info(), - types.Settings(conf)) +settings = Settings(DEFAULT_SETTINGS) + + +def init_settings(user_dir): + """Fills `settings` with values from `settings.py` and env.""" + from .logs import exception try: - conf.update(_settings_from_env()) + settings.update(_settings_from_file(user_dir)) except Exception: - logs.exception("Can't load settings from env", - sys.exc_info(), - types.Settings(conf)) + exception("Can't load settings from file", sys.exc_info()) - if not isinstance(conf['rules'], types.RulesNamesList): - conf['rules'] = types.RulesNamesList(conf['rules']) + try: + settings.update(_settings_from_env()) + except Exception: + exception("Can't load settings from env", sys.exc_info()) - if not isinstance(conf['exclude_rules'], types.RulesNamesList): - conf['exclude_rules'] = types.RulesNamesList(conf['exclude_rules']) + if not isinstance(settings['rules'], RulesNamesList): + settings.rules = RulesNamesList(settings['rules']) - return types.Settings(conf) + if not isinstance(settings.exclude_rules, RulesNamesList): + settings.exclude_rules = RulesNamesList(settings.exclude_rules) def initialize_settings_file(user_dir): diff --git a/thefuck/corrector.py b/thefuck/corrector.py index de442cd2..8840749a 100644 --- a/thefuck/corrector.py +++ b/thefuck/corrector.py @@ -1,16 +1,18 @@ import sys from imp import load_source from pathlib import Path -from . import conf, types, logs +from .conf import settings, DEFAULT_PRIORITY +from .types import Rule, CorrectedCommand, SortedCorrectedCommandsSequence +from . import logs -def load_rule(rule, settings): +def load_rule(rule): """Imports rule module and returns it.""" name = rule.name[:-3] - with logs.debug_time(u'Importing rule: {};'.format(name), settings): + with logs.debug_time(u'Importing rule: {};'.format(name)): rule_module = load_source(name, str(rule)) - priority = getattr(rule_module, 'priority', conf.DEFAULT_PRIORITY) - return types.Rule(name, rule_module.match, + 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), @@ -18,27 +20,27 @@ def load_rule(rule, settings): getattr(rule_module, 'requires_output', True)) -def get_loaded_rules(rules, settings): +def get_loaded_rules(rules): """Yields all available rules.""" for rule in rules: if rule.name != '__init__.py': - loaded_rule = load_rule(rule, settings) + loaded_rule = load_rule(rule) if loaded_rule in settings.rules and \ - loaded_rule not in settings.exclude_rules: + loaded_rule not in settings.exclude_rules: yield loaded_rule -def get_rules(user_dir, settings): +def get_rules(user_dir): """Returns all enabled rules.""" bundled = Path(__file__).parent \ .joinpath('rules') \ .glob('*.py') user = user_dir.joinpath('rules').glob('*.py') - return sorted(get_loaded_rules(sorted(bundled) + sorted(user), settings), + return sorted(get_loaded_rules(sorted(bundled) + sorted(user)), key=lambda rule: rule.priority) -def is_rule_match(command, rule, settings): +def is_rule_match(command, rule): """Returns first matched rule for command.""" script_only = command.stdout is None and command.stderr is None @@ -46,27 +48,26 @@ def is_rule_match(command, rule, settings): return False try: - with logs.debug_time(u'Trying rule: {};'.format(rule.name), - settings): + with logs.debug_time(u'Trying rule: {};'.format(rule.name)): if rule.match(command, settings): return True except Exception: - logs.rule_failed(rule, sys.exc_info(), settings) + logs.rule_failed(rule, sys.exc_info()) -def make_corrected_commands(command, rule, settings): +def make_corrected_commands(command, rule): new_commands = rule.get_new_command(command, settings) if not isinstance(new_commands, list): new_commands = (new_commands,) for n, new_command in enumerate(new_commands): - yield types.CorrectedCommand(script=new_command, - side_effect=rule.side_effect, - priority=(n + 1) * rule.priority) + yield CorrectedCommand(script=new_command, + side_effect=rule.side_effect, + priority=(n + 1) * rule.priority) -def get_corrected_commands(command, user_dir, settings): +def get_corrected_commands(command, user_dir): corrected_commands = ( - corrected for rule in get_rules(user_dir, settings) - if is_rule_match(command, rule, settings) - for corrected in make_corrected_commands(command, rule, settings)) - return types.SortedCorrectedCommandsSequence(corrected_commands, settings) + corrected for rule in get_rules(user_dir) + if is_rule_match(command, rule) + for corrected in make_corrected_commands(command, rule)) + return SortedCorrectedCommandsSequence(corrected_commands) diff --git a/thefuck/logs.py b/thefuck/logs.py index 4db63486..93162979 100644 --- a/thefuck/logs.py +++ b/thefuck/logs.py @@ -5,9 +5,10 @@ from datetime import datetime import sys from traceback import format_exception import colorama +from .conf import settings -def color(color_, settings): +def color(color_): """Utility for ability to disabling colored output.""" if settings.no_colors: return '' @@ -15,37 +16,37 @@ def color(color_, settings): return color_ -def exception(title, exc_info, settings): +def exception(title, exc_info): sys.stderr.write( u'{warn}[WARN] {title}:{reset}\n{trace}' u'{warn}----------------------------{reset}\n\n'.format( warn=color(colorama.Back.RED + colorama.Fore.WHITE - + colorama.Style.BRIGHT, settings), - reset=color(colorama.Style.RESET_ALL, settings), + + colorama.Style.BRIGHT), + reset=color(colorama.Style.RESET_ALL), title=title, trace=''.join(format_exception(*exc_info)))) -def rule_failed(rule, exc_info, settings): - exception('Rule {}'.format(rule.name), exc_info, settings) +def rule_failed(rule, exc_info): + exception('Rule {}'.format(rule.name), exc_info) -def failed(msg, settings): +def failed(msg): sys.stderr.write('{red}{msg}{reset}\n'.format( msg=msg, - red=color(colorama.Fore.RED, settings), - reset=color(colorama.Style.RESET_ALL, settings))) + red=color(colorama.Fore.RED), + reset=color(colorama.Style.RESET_ALL))) -def show_corrected_command(corrected_command, settings): +def show_corrected_command(corrected_command): sys.stderr.write('{bold}{script}{reset}{side_effect}\n'.format( script=corrected_command.script, side_effect=' (+side effect)' if corrected_command.side_effect else '', - bold=color(colorama.Style.BRIGHT, settings), - reset=color(colorama.Style.RESET_ALL, settings))) + bold=color(colorama.Style.BRIGHT), + reset=color(colorama.Style.RESET_ALL))) -def confirm_text(corrected_command, settings): +def confirm_text(corrected_command): sys.stderr.write( ('{clear}{bold}{script}{reset}{side_effect} ' '[{green}enter{reset}/{blue}↑{reset}/{blue}↓{reset}' @@ -53,42 +54,42 @@ def confirm_text(corrected_command, settings): script=corrected_command.script, side_effect=' (+side effect)' if corrected_command.side_effect else '', clear='\033[1K\r', - bold=color(colorama.Style.BRIGHT, settings), - green=color(colorama.Fore.GREEN, settings), - red=color(colorama.Fore.RED, settings), - reset=color(colorama.Style.RESET_ALL, settings), - blue=color(colorama.Fore.BLUE, settings))) + bold=color(colorama.Style.BRIGHT), + green=color(colorama.Fore.GREEN), + red=color(colorama.Fore.RED), + reset=color(colorama.Style.RESET_ALL), + blue=color(colorama.Fore.BLUE))) -def debug(msg, settings): +def debug(msg): if settings.debug: sys.stderr.write(u'{blue}{bold}DEBUG:{reset} {msg}\n'.format( msg=msg, - reset=color(colorama.Style.RESET_ALL, settings), - blue=color(colorama.Fore.BLUE, settings), - bold=color(colorama.Style.BRIGHT, settings))) + reset=color(colorama.Style.RESET_ALL), + blue=color(colorama.Fore.BLUE), + bold=color(colorama.Style.BRIGHT))) @contextmanager -def debug_time(msg, settings): +def debug_time(msg): started = datetime.now() try: yield finally: - debug(u'{} took: {}'.format(msg, datetime.now() - started), settings) + debug(u'{} took: {}'.format(msg, datetime.now() - started)) -def how_to_configure_alias(configuration_details, settings): +def how_to_configure_alias(configuration_details): print("Seems like {bold}fuck{reset} alias isn't configured!".format( - bold=color(colorama.Style.BRIGHT, settings), - reset=color(colorama.Style.RESET_ALL, settings))) + bold=color(colorama.Style.BRIGHT), + reset=color(colorama.Style.RESET_ALL))) if configuration_details: content, path = configuration_details print( "Please put {bold}{content}{reset} in your " "{bold}{path}{reset}.".format( - bold=color(colorama.Style.BRIGHT, settings), - reset=color(colorama.Style.RESET_ALL, settings), + bold=color(colorama.Style.BRIGHT), + reset=color(colorama.Style.RESET_ALL), path=path, content=content)) print('More details - https://github.com/nvbn/thefuck#manual-installation') diff --git a/thefuck/main.py b/thefuck/main.py index 046973a1..262dcee5 100644 --- a/thefuck/main.py +++ b/thefuck/main.py @@ -10,7 +10,8 @@ import sys from psutil import Process, TimeoutExpired import colorama import six -from . import logs, conf, types, shells +from . import logs, types, shells +from .conf import initialize_settings_file, init_settings, settings from .corrector import get_corrected_commands from .ui import select_command @@ -21,11 +22,11 @@ def setup_user_dir(): rules_dir = user_dir.joinpath('rules') if not rules_dir.is_dir(): rules_dir.mkdir(parents=True) - conf.initialize_settings_file(user_dir) + initialize_settings_file(user_dir) return user_dir -def wait_output(settings, popen): +def wait_output(popen): """Returns `True` if we can get output of the command in the `settings.wait_command` time. @@ -43,7 +44,7 @@ def wait_output(settings, popen): return False -def get_command(settings, args): +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:]) @@ -58,23 +59,22 @@ def get_command(settings, args): env = dict(os.environ) env.update(settings.env) - with logs.debug_time(u'Call: {}; with env: {};'.format(script, env), - settings): + 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(settings, result): + 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), settings) - logs.debug(u'Received stderr: {}'.format(stderr), settings) + 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!', settings) + logs.debug(u'Execution timed out!') return types.Command(script, None, None) -def run_command(old_cmd, command, settings): +def run_command(old_cmd, command): """Runs command from rule for passed command.""" if command.side_effect: command.side_effect(old_cmd, command.script, settings) @@ -87,20 +87,20 @@ def run_command(old_cmd, command, settings): def fix_command(): colorama.init() user_dir = setup_user_dir() - settings = conf.get_settings(user_dir) - with logs.debug_time('Total', settings): - logs.debug(u'Run with settings: {}'.format(pformat(settings)), settings) + init_settings(user_dir) + with logs.debug_time('Total'): + logs.debug(u'Run with settings: {}'.format(pformat(settings))) - command = get_command(settings, sys.argv) + command = get_command(sys.argv) if not command: - logs.debug('Empty command, nothing to do', settings) + logs.debug('Empty command, nothing to do') return - corrected_commands = get_corrected_commands(command, user_dir, settings) - selected_command = select_command(corrected_commands, settings) + corrected_commands = get_corrected_commands(command, user_dir) + selected_command = select_command(corrected_commands) if selected_command: - run_command(command, selected_command, settings) + run_command(command, selected_command) def _get_current_version(): @@ -128,8 +128,8 @@ def how_to_configure_alias(): """ colorama.init() user_dir = setup_user_dir() - settings = conf.get_settings(user_dir) - logs.how_to_configure_alias(shells.how_to_configure(), settings) + init_settings(user_dir) + logs.how_to_configure_alias(shells.how_to_configure()) def main(): diff --git a/thefuck/types.py b/thefuck/types.py index cf091376..8e302412 100644 --- a/thefuck/types.py +++ b/thefuck/types.py @@ -1,6 +1,5 @@ from collections import namedtuple from traceback import format_stack -from .logs import debug Command = namedtuple('Command', ('script', 'stdout', 'stderr')) @@ -8,6 +7,7 @@ Rule = namedtuple('Rule', ('name', 'match', 'get_new_command', 'enabled_by_default', 'side_effect', 'priority', 'requires_output')) + class CorrectedCommand(object): def __init__(self, script, side_effect, priority): self.script = script @@ -17,7 +17,7 @@ class CorrectedCommand(object): def __eq__(self, other): """Ignores `priority` field.""" if isinstance(other, CorrectedCommand): - return (other.script, other.side_effect) ==\ + return (other.script, other.side_effect) == \ (self.script, self.side_effect) else: return False @@ -41,13 +41,8 @@ class Settings(dict): def __getattr__(self, item): return self.get(item) - def update(self, **kwargs): - """ - Returns new settings with values from `kwargs` for unset settings. - """ - conf = dict(kwargs) - conf.update(self) - return Settings(conf) + def __setattr__(self, key, value): + self[key] = value class SortedCorrectedCommandsSequence(object): @@ -59,8 +54,7 @@ class SortedCorrectedCommandsSequence(object): """ - def __init__(self, commands, settings): - self._settings = settings + def __init__(self, commands): self._commands = commands self._cached = self._realise_first() self._realised = False @@ -81,13 +75,15 @@ class SortedCorrectedCommandsSequence(object): 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())), self._settings) + self._cached, '\n'.join(format_stack()))) def __getitem__(self, item): if item != 0 and not self._realised: diff --git a/thefuck/ui.py b/thefuck/ui.py index 36dced97..4cd73037 100644 --- a/thefuck/ui.py +++ b/thefuck/ui.py @@ -1,6 +1,7 @@ # -*- encoding: utf-8 -*- import sys +from .conf import settings from . import logs try: @@ -71,7 +72,7 @@ class CommandSelector(object): fn(self.value) -def select_command(corrected_commands, settings): +def select_command(corrected_commands): """Returns: - the first command when confirmation disabled; @@ -80,21 +81,21 @@ def select_command(corrected_commands, settings): """ if not corrected_commands: - logs.failed('No fucks given', settings) + logs.failed('No fucks given') return selector = CommandSelector(corrected_commands) if not settings.require_confirmation: - logs.show_corrected_command(selector.value, settings) + logs.show_corrected_command(selector.value) return selector.value - selector.on_change(lambda val: logs.confirm_text(val, settings)) + selector.on_change(lambda val: logs.confirm_text(val)) for action in read_actions(): if action == SELECT: sys.stderr.write('\n') return selector.value elif action == ABORT: - logs.failed('\nAborted', settings) + logs.failed('\nAborted') return elif action == PREVIOUS: selector.previous() diff --git a/thefuck/utils.py b/thefuck/utils.py index 57fe2f50..59cfcf7f 100644 --- a/thefuck/utils.py +++ b/thefuck/utils.py @@ -12,6 +12,7 @@ import re from pathlib import Path import pkg_resources import six +from .types import Settings DEVNULL = open(os.devnull, 'w') @@ -70,7 +71,7 @@ def wrap_settings(params): """ def _wrap_settings(fn, command, settings): - return fn(command, settings.update(**params)) + return fn(command, Settings(settings, **params)) return decorator(_wrap_settings)