1
0
mirror of https://github.com/nvbn/thefuck.git synced 2025-02-22 12:58:33 +00:00

Add ability to change settings via environment variables

This commit is contained in:
nvbn 2015-04-22 20:18:53 +02:00
parent b4b599df80
commit 69a9516477
8 changed files with 233 additions and 61 deletions

View File

@ -196,11 +196,18 @@ def get_new_command(command, settings):
The Fuck has a few settings parameters, they can be changed in `~/.thefuck/settings.py`: The Fuck has a few settings parameters, they can be changed in `~/.thefuck/settings.py`:
* `rules` – list of enabled rules, by default all; * `rules` – list of enabled rules, by default `thefuck.conf.DEFAULT`;
* `require_confirmation` – require confirmation before running new command, by default `False`; * `require_confirmation` – require confirmation before running new command, by default `False`;
* `wait_command` – max amount of time in seconds for getting previous command output; * `wait_command` – max amount of time in seconds for getting previous command output;
* `no_colors` – disable colored output. * `no_colors` – disable colored output.
Or via environment variables:
* `THEFUCK_RULES` – list of enabled rules, like `DEFAULT:rm_root` or `sudo:no_command`;
* `THEFUCK_REQUIRE_CONFIRMATION` – require confirmation before running new command, `true/false`;
* `THEFUCK_WAIT_COMMAND` – max amount of time in seconds for getting previous command output;
* `THEFUCK_NO_COLORS` – disable colored output, `true/false`.
## Developing ## Developing
Install `The Fuck` for development: Install `The Fuck` for development:

75
tests/test_conf.py Normal file
View File

@ -0,0 +1,75 @@
from mock import patch, Mock
from thefuck.main import Rule
from thefuck import conf
def test_rules_list():
assert conf.RulesList(['bash', 'lisp']) == ['bash', 'lisp']
assert conf.RulesList(['bash', 'lisp']) == conf.RulesList(['bash', 'lisp'])
assert Rule('lisp', None, None, False) in conf.RulesList(['lisp'])
assert Rule('bash', None, None, False) not in conf.RulesList(['lisp'])
def test_default():
assert Rule('test', None, None, True) in conf.DEFAULT
assert Rule('test', None, None, False) not in conf.DEFAULT
assert Rule('test', None, None, False) in (conf.DEFAULT + ['test'])
def test_settings_defaults():
with patch('thefuck.conf.load_source', return_value=object()), \
patch('thefuck.conf.os.environ', new_callable=lambda: {}):
for key, val in conf.Settings.defaults.items():
assert getattr(conf.Settings(Mock()), key) == val
def test_settings_from_file():
with patch('thefuck.conf.load_source', return_value=Mock(rules=['test'],
wait_command=10,
require_confirmation=True,
no_colors=True)), \
patch('thefuck.conf.os.environ', new_callable=lambda: {}):
settings = conf.Settings(Mock())
assert settings.rules == ['test']
assert settings.wait_command == 10
assert settings.require_confirmation is True
assert settings.no_colors is True
def test_settings_from_file_with_DEFAULT():
with patch('thefuck.conf.load_source', return_value=Mock(rules=conf.DEFAULT + ['test'],
wait_command=10,
require_confirmation=True,
no_colors=True)), \
patch('thefuck.conf.os.environ', new_callable=lambda: {}):
settings = conf.Settings(Mock())
assert settings.rules == conf.DEFAULT + ['test']
def test_settings_from_env():
with patch('thefuck.conf.load_source', return_value=Mock(rules=['test'],
wait_command=10)), \
patch('thefuck.conf.os.environ',
new_callable=lambda: {'THEFUCK_RULES': 'bash:lisp',
'THEFUCK_WAIT_COMMAND': '55',
'THEFUCK_REQUIRE_CONFIRMATION': 'true',
'THEFUCK_NO_COLORS': 'false'}):
settings = conf.Settings(Mock())
assert settings.rules == ['bash', 'lisp']
assert settings.wait_command == 55
assert settings.require_confirmation is True
assert settings.no_colors is False
def test_settings_from_env_with_DEFAULT():
with patch('thefuck.conf.load_source', return_value=Mock()), \
patch('thefuck.conf.os.environ', new_callable=lambda: {'THEFUCK_RULES': 'DEFAULT:bash:lisp'}):
settings = conf.Settings(Mock())
assert settings.rules == conf.DEFAULT + ['bash', 'lisp']
def test_update_settings():
settings = conf.BaseSettings({'key': 'val'})
new_settings = settings.update(key='new-val')
assert new_settings.key == 'new-val'
assert settings.key == 'val'

View File

@ -1,25 +1,7 @@
from subprocess import PIPE from subprocess import PIPE
from pathlib import PosixPath, Path from pathlib import PosixPath, Path
from mock import patch, Mock from mock import patch, Mock
from thefuck import main from thefuck import main, conf
def test_get_settings():
with patch('thefuck.main.load_source', return_value=Mock(rules=['bash'])):
assert main.get_settings(Path('/')).rules == ['bash']
with patch('thefuck.main.load_source', return_value=Mock(spec=[])):
assert main.get_settings(Path('/')).rules is None
def test_is_rule_enabled():
assert main.is_rule_enabled(Mock(rules=None),
main.Rule('bash', None, None, True))
assert not main.is_rule_enabled(Mock(rules=None),
main.Rule('bash', None, None, False))
assert main.is_rule_enabled(Mock(rules=['bash']),
main.Rule('bash', None, None, True))
assert not main.is_rule_enabled(Mock(rules=['bash']),
main.Rule('lisp', None, None, True))
def test_load_rule(): def test_load_rule():
@ -43,13 +25,15 @@ def test_get_rules():
glob.return_value = [PosixPath('bash.py'), PosixPath('lisp.py')] glob.return_value = [PosixPath('bash.py'), PosixPath('lisp.py')]
assert list(main.get_rules( assert list(main.get_rules(
Path('~'), Path('~'),
Mock(rules=None))) == [main.Rule('bash', 'bash', 'bash', True), Mock(rules=conf.DEFAULT))) \
== [main.Rule('bash', 'bash', 'bash', True),
main.Rule('lisp', 'lisp', 'lisp', True), main.Rule('lisp', 'lisp', 'lisp', True),
main.Rule('bash', 'bash', 'bash', True), main.Rule('bash', 'bash', 'bash', True),
main.Rule('lisp', 'lisp', 'lisp', True)] main.Rule('lisp', 'lisp', 'lisp', True)]
assert list(main.get_rules( assert list(main.get_rules(
Path('~'), Path('~'),
Mock(rules=['bash']))) == [main.Rule('bash', 'bash', 'bash', True), Mock(rules=conf.RulesList(['bash'])))) \
== [main.Rule('bash', 'bash', 'bash', True),
main.Rule('bash', 'bash', 'bash', True)] main.Rule('bash', 'bash', 'bash', True)]

View File

@ -1,6 +1,15 @@
from mock import Mock from mock import Mock
from thefuck.utils import sudo_support from thefuck.utils import sudo_support, wrap_settings
from thefuck.main import Command from thefuck.main import Command
from thefuck.conf import BaseSettings
def test_wrap_settings():
fn = lambda _, settings: settings._conf
assert wrap_settings({'key': 'val'})(fn)(None, BaseSettings({})) \
== {'key': 'val'}
assert wrap_settings({'key': 'new-val'})(fn)(
None, BaseSettings({'key': 'val'})) == {'key': 'new-val'}
def test_sudo_support(): def test_sudo_support():

120
thefuck/conf.py Normal file
View File

@ -0,0 +1,120 @@
from copy import copy
from imp import load_source
import os
import sys
from six import text_type
from . import logs
class RulesList(object):
"""Wrapper a top of list for string rules names."""
def __init__(self, rules):
self.rules = rules
def __contains__(self, item):
return item.name in self.rules
def __getattr__(self, item):
return getattr(self.rules, item)
def __eq__(self, other):
return self.rules == other
class _DefaultRules(RulesList):
def __add__(self, items):
return _DefaultRules(self.rules + items)
def __contains__(self, item):
return item.enabled_by_default or \
super(_DefaultRules, self).__contains__(item)
def __eq__(self, other):
if isinstance(other, _DefaultRules):
return self.rules == other.rules
else:
return False
DEFAULT = _DefaultRules([])
class BaseSettings(object):
def __init__(self, conf):
self._conf = conf
def __getattr__(self, item):
return self._conf.get(item)
def update(self, **kwargs):
"""Returns new settings with new values from `kwargs`."""
conf = copy(self._conf)
conf.update(kwargs)
return BaseSettings(conf)
class Settings(BaseSettings):
"""Settings loaded from defaults/file/env."""
defaults = {'rules': DEFAULT,
'wait_command': 3,
'require_confirmation': False,
'no_colors': False}
env_to_attr = {'THEFUCK_RULES': 'rules',
'THEFUCK_WAIT_COMMAND': 'wait_command',
'THEFUCK_REQUIRE_CONFIRMATION': 'require_confirmation',
'THEFUCK_NO_COLORS': 'no_colors'}
def __init__(self, user_dir):
super(Settings, self).__init__(self._load_conf(user_dir))
def _load_conf(self, user_dir):
conf = copy(self.defaults)
try:
conf.update(self._load_from_file(user_dir))
except:
logs.exception("Can't load settings from file",
sys.exc_info(),
BaseSettings(conf))
try:
conf.update(self._load_from_env())
except:
logs.exception("Can't load settings from env",
sys.exc_info(),
BaseSettings(conf))
if not isinstance(conf['rules'], RulesList):
conf['rules'] = RulesList(conf['rules'])
return conf
def _load_from_file(self, user_dir):
"""Loads settings from file."""
settings = load_source('settings',
text_type(user_dir.joinpath('settings.py')))
return {key: getattr(settings, key)
for key in self.defaults.keys()
if hasattr(settings, key)}
def _load_from_env(self):
"""Loads settings from env."""
return {attr: self._val_from_env(env, attr)
for env, attr in self.env_to_attr.items()
if env in os.environ}
def _val_from_env(self, env, attr):
"""Transforms env-strings to python."""
val = os.environ[env]
if attr == 'rules':
val = self._rules_from_env(val)
elif attr == 'wait_command':
val = int(val)
elif attr in ('require_confirmation', 'no_colors'):
val = val.lower() == 'true'
return val
def _rules_from_env(self, val):
"""Transforms rules list from env-string to python."""
val = val.split(':')
if 'DEFAULT' in val:
val = DEFAULT + [rule for rule in val if rule != 'DEFAULT']
return val

View File

@ -11,17 +11,21 @@ def color(color_, settings):
return color_ return color_
def rule_failed(rule, exc_info, settings): def exception(title, exc_info, settings):
sys.stderr.write( sys.stderr.write(
u'{warn}[WARN] Rule {name}:{reset}\n{trace}' u'{warn}[WARN] {title}:{reset}\n{trace}'
u'{warn}----------------------------{reset}\n\n'.format( u'{warn}----------------------------{reset}\n\n'.format(
warn=color(colorama.Back.RED + colorama.Fore.WHITE warn=color(colorama.Back.RED + colorama.Fore.WHITE
+ colorama.Style.BRIGHT, settings), + colorama.Style.BRIGHT, settings),
reset=color(colorama.Style.RESET_ALL, settings), reset=color(colorama.Style.RESET_ALL, settings),
name=rule.name, title=title,
trace=''.join(format_exception(*exc_info)))) trace=''.join(format_exception(*exc_info))))
def rule_failed(rule, exc_info, settings):
exception('Rule {}'.format(rule.name), exc_info, settings)
def show_command(new_command, settings): def show_command(new_command, settings):
sys.stderr.write('{bold}{command}{reset}\n'.format( sys.stderr.write('{bold}{command}{reset}\n'.format(
command=new_command, command=new_command,

View File

@ -7,7 +7,7 @@ import os
import sys import sys
from psutil import Process, TimeoutExpired from psutil import Process, TimeoutExpired
import colorama import colorama
from thefuck import logs from . import logs, conf
Command = namedtuple('Command', ('script', 'stdout', 'stderr')) Command = namedtuple('Command', ('script', 'stdout', 'stderr'))
@ -25,30 +25,6 @@ def setup_user_dir():
return user_dir return user_dir
def get_settings(user_dir):
"""Returns prepared settings module."""
settings = load_source('settings',
str(user_dir.joinpath('settings.py')))
settings.__dict__.setdefault('rules', None)
settings.__dict__.setdefault('wait_command', 3)
settings.__dict__.setdefault('require_confirmation', False)
settings.__dict__.setdefault('no_colors', False)
return settings
def is_rule_enabled(settings, rule):
"""Returns `True` when rule mentioned in `rules` or `rules`
isn't defined.
"""
if settings.rules is None and rule.enabled_by_default:
return True
elif settings.rules and rule.name in settings.rules:
return True
else:
return False
def load_rule(rule): def load_rule(rule):
"""Imports rule module and returns it.""" """Imports rule module and returns it."""
rule_module = load_source(rule.name[:-3], str(rule)) rule_module = load_source(rule.name[:-3], str(rule))
@ -66,7 +42,7 @@ def get_rules(user_dir, settings):
for rule in sorted(list(bundled)) + list(user): for rule in sorted(list(bundled)) + list(user):
if rule.name != '__init__.py': if rule.name != '__init__.py':
loaded_rule = load_rule(rule) loaded_rule = load_rule(rule)
if is_rule_enabled(settings, loaded_rule): if loaded_rule in settings.rules:
yield loaded_rule yield loaded_rule
@ -145,7 +121,7 @@ def is_second_run(command):
def main(): def main():
colorama.init() colorama.init()
user_dir = setup_user_dir() user_dir = setup_user_dir()
settings = get_settings(user_dir) settings = conf.Settings(user_dir)
command = get_command(settings, sys.argv) command = get_command(settings, sys.argv)
if command: if command:

View File

@ -37,10 +37,7 @@ def wrap_settings(params):
def decorator(fn): def decorator(fn):
@wraps(fn) @wraps(fn)
def wrapper(command, settings): def wrapper(command, settings):
for key, val in params.items(): return fn(command, settings.update(**params))
if not hasattr(settings, key):
setattr(settings, key, val)
return fn(command, settings)
return wrapper return wrapper
return decorator return decorator