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

Make settings a global singleton

This commit is contained in:
nvbn 2015-09-06 21:47:12 +03:00
parent 191a2e588d
commit 105d3d8137
8 changed files with 114 additions and 119 deletions

View File

@ -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']

View File

@ -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):

View File

@ -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)

View File

@ -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')

View File

@ -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():

View File

@ -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:

View File

@ -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()

View File

@ -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)