mirror of
https://github.com/nvbn/thefuck.git
synced 2025-02-21 20:38:54 +00:00
Add ability to change settings via environment variables
This commit is contained in:
parent
b4b599df80
commit
69a9516477
@ -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
75
tests/test_conf.py
Normal 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'
|
@ -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,14 +25,16 @@ 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('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)]
|
main.Rule('bash', 'bash', 'bash', 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)]
|
||||||
|
|
||||||
|
|
||||||
def test_get_command():
|
def test_get_command():
|
||||||
|
@ -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
120
thefuck/conf.py
Normal 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
|
@ -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,
|
||||||
|
@ -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:
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user