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

#611: Allow to configure alias automatically by calling fuck twice

This commit is contained in:
Vladimir Iakovlev 2017-03-13 21:50:13 +01:00
parent 2379573cf2
commit 14a9cd85aa
19 changed files with 336 additions and 54 deletions

View File

@ -51,4 +51,4 @@ setup(name='thefuck',
extras_require=extras_require, extras_require=extras_require,
entry_points={'console_scripts': [ entry_points={'console_scripts': [
'thefuck = thefuck.main:main', 'thefuck = thefuck.main:main',
'fuck = thefuck.main:how_to_configure_alias']}) 'fuck = thefuck.not_configured:main']})

View File

@ -20,3 +20,11 @@ def history_lines(mocker):
.return_value.readlines.return_value = lines .return_value.readlines.return_value = lines
return aux return aux
@pytest.fixture
def config_exists(mocker):
path_mock = mocker.patch('thefuck.shells.generic.Path')
return path_mock.return_value \
.expanduser.return_value \
.exists

View File

@ -61,3 +61,12 @@ class TestBash(object):
command = 'git log -p' command = 'git log -p'
command_parts = ['git', 'log', '-p'] command_parts = ['git', 'log', '-p']
assert shell.split_command(command) == command_parts assert shell.split_command(command) == command_parts
def test_how_to_configure(self, shell, config_exists):
config_exists.return_value = True
assert shell.how_to_configure().can_configure_automatically
def test_how_to_configure_when_config_not_found(self, shell,
config_exists):
config_exists.return_value = False
assert not shell.how_to_configure().can_configure_automatically

View File

@ -95,3 +95,12 @@ class TestFish(object):
shell.put_to_history(entry) shell.put_to_history(entry)
builtins_open.return_value.__enter__.return_value. \ builtins_open.return_value.__enter__.return_value. \
write.assert_called_once_with(entry_utf8) write.assert_called_once_with(entry_utf8)
def test_how_to_configure(self, shell, config_exists):
config_exists.return_value = True
assert shell.how_to_configure().can_configure_automatically
def test_how_to_configure_when_config_not_found(self, shell,
config_exists):
config_exists.return_value = False
assert not shell.how_to_configure().can_configure_automatically

View File

@ -37,3 +37,6 @@ class TestGeneric(object):
def test_split_command(self, shell): def test_split_command(self, shell):
assert shell.split_command('ls') == ['ls'] assert shell.split_command('ls') == ['ls']
assert shell.split_command(u'echo café') == [u'echo', u'café'] assert shell.split_command(u'echo café') == [u'echo', u'café']
def test_how_to_configure(self, shell):
assert shell.how_to_configure() is None

View File

@ -17,3 +17,6 @@ class TestPowershell(object):
assert 'function fuck' in shell.app_alias('fuck') assert 'function fuck' in shell.app_alias('fuck')
assert 'function FUCK' in shell.app_alias('FUCK') assert 'function FUCK' in shell.app_alias('FUCK')
assert 'thefuck' in shell.app_alias('fuck') assert 'thefuck' in shell.app_alias('fuck')
def test_how_to_configure(self, shell):
assert not shell.how_to_configure().can_configure_automatically

View File

@ -48,3 +48,12 @@ class TestTcsh(object):
def test_get_history(self, history_lines, shell): def test_get_history(self, history_lines, shell):
history_lines(['ls', 'rm']) history_lines(['ls', 'rm'])
assert list(shell.get_history()) == ['ls', 'rm'] assert list(shell.get_history()) == ['ls', 'rm']
def test_how_to_configure(self, shell, config_exists):
config_exists.return_value = True
assert shell.how_to_configure().can_configure_automatically
def test_how_to_configure_when_config_not_found(self, shell,
config_exists):
config_exists.return_value = False
assert not shell.how_to_configure().can_configure_automatically

View File

@ -55,3 +55,12 @@ class TestZsh(object):
def test_get_history(self, history_lines, shell): def test_get_history(self, history_lines, shell):
history_lines([': 1432613911:0;ls', ': 1432613916:0;rm']) history_lines([': 1432613911:0;ls', ': 1432613916:0;rm'])
assert list(shell.get_history()) == ['ls', 'rm'] assert list(shell.get_history()) == ['ls', 'rm']
def test_how_to_configure(self, shell, config_exists):
config_exists.return_value = True
assert shell.how_to_configure().can_configure_automatically
def test_how_to_configure_when_config_not_found(self, shell,
config_exists):
config_exists.return_value = False
assert not shell.how_to_configure().can_configure_automatically

View File

@ -0,0 +1,119 @@
import pytest
from thefuck.shells.generic import ShellConfiguration
from thefuck.not_configured import main
@pytest.fixture(autouse=True)
def usage_tracker(mocker):
return mocker.patch(
'thefuck.not_configured._get_not_configured_usage_tracker_path')
def _assert_tracker_updated(usage_tracker, pid):
usage_tracker.return_value \
.open.return_value \
.__enter__.return_value \
.write.assert_called_once_with(str(pid))
def _change_tracker(usage_tracker, pid):
usage_tracker.return_value.exists.return_value = True
usage_tracker.return_value \
.open.return_value \
.__enter__.return_value \
.read.return_value = str(pid)
@pytest.fixture(autouse=True)
def shell_pid(mocker):
return mocker.patch('thefuck.not_configured._get_shell_pid')
@pytest.fixture(autouse=True)
def shell(mocker):
shell = mocker.patch('thefuck.not_configured.shell')
shell.get_history.return_value = []
shell.how_to_configure.return_value = ShellConfiguration(
content='eval $(thefuck --alias)',
path='/tmp/.bashrc',
reload='bash',
can_configure_automatically=True)
return shell
@pytest.fixture(autouse=True)
def shell_config(mocker):
path_mock = mocker.patch('thefuck.not_configured.Path')
return path_mock.return_value \
.expanduser.return_value \
.open.return_value \
.__enter__.return_value
@pytest.fixture(autouse=True)
def logs(mocker):
return mocker.patch('thefuck.not_configured.logs')
def test_for_generic_shell(shell, logs):
shell.how_to_configure.return_value = None
main()
logs.how_to_configure_alias.assert_called_once()
def test_on_first_run(usage_tracker, shell_pid, logs):
shell_pid.return_value = 12
usage_tracker.return_value.exists.return_value = False
main()
_assert_tracker_updated(usage_tracker, 12)
logs.how_to_configure_alias.assert_called_once()
def test_on_run_after_other_commands(usage_tracker, shell_pid, shell, logs):
shell_pid.return_value = 12
shell.get_history.return_value = ['fuck', 'ls']
_change_tracker(usage_tracker, 12)
main()
logs.how_to_configure_alias.assert_called_once()
def test_on_first_run_from_current_shell(usage_tracker, shell_pid,
shell, logs):
shell.get_history.return_value = ['fuck']
shell_pid.return_value = 12
_change_tracker(usage_tracker, 55)
main()
_assert_tracker_updated(usage_tracker, 12)
logs.how_to_configure_alias.assert_called_once()
def test_when_cant_configure_automatically(shell_pid, shell, logs):
shell_pid.return_value = 12
shell.how_to_configure.return_value = ShellConfiguration(
content='eval $(thefuck --alias)',
path='/tmp/.bashrc',
reload='bash',
can_configure_automatically=False)
main()
logs.how_to_configure_alias.assert_called_once()
def test_when_already_configured(usage_tracker, shell_pid,
shell, shell_config, logs):
shell.get_history.return_value = ['fuck']
shell_pid.return_value = 12
_change_tracker(usage_tracker, 12)
shell_config.read.return_value = 'eval $(thefuck --alias)'
main()
logs.already_configured.assert_called_once()
def test_when_successfuly_configured(usage_tracker, shell_pid,
shell, shell_config, logs):
shell.get_history.return_value = ['fuck']
shell_pid.return_value = 12
_change_tracker(usage_tracker, 12)
shell_config.read.return_value = ''
main()
shell_config.write.assert_any_call('eval $(thefuck --alias)')
logs.configured_successfully.assert_called_once()

View File

@ -91,6 +91,33 @@ def how_to_configure_alias(configuration_details):
"changes with {bold}{reload}{reset} or restart your shell.".format( "changes with {bold}{reload}{reset} or restart your shell.".format(
bold=color(colorama.Style.BRIGHT), bold=color(colorama.Style.BRIGHT),
reset=color(colorama.Style.RESET_ALL), reset=color(colorama.Style.RESET_ALL),
**configuration_details)) **configuration_details._asdict()))
if configuration_details.can_configure_automatically:
print(
"Or run {bold}fuck{reset} second time for configuring"
" it automatically.".format(
bold=color(colorama.Style.BRIGHT),
reset=color(colorama.Style.RESET_ALL)))
print('More details - https://github.com/nvbn/thefuck#manual-installation') print('More details - https://github.com/nvbn/thefuck#manual-installation')
def already_configured(configuration_details):
print(
"Seems like {bold}fuck{reset} alias already configured!\n"
"For applying changes run {bold}{reload}{reset}"
" or restart your shell.".format(
bold=color(colorama.Style.BRIGHT),
reset=color(colorama.Style.RESET_ALL),
reload=configuration_details.reload))
def configured_successfully(configuration_details):
print(
"{bold}fuck{reset} alias configured successfully!\n"
"For applying changes run {bold}{reload}{reset}"
" or restart your shell.".format(
bold=color(colorama.Style.BRIGHT),
reset=color(colorama.Style.RESET_ALL),
reload=configuration_details.reload))

View File

@ -46,16 +46,6 @@ def print_alias():
print(shell.app_alias(alias)) print(shell.app_alias(alias))
def how_to_configure_alias():
"""Shows useful information about how-to configure alias.
It'll be only visible when user type fuck and when alias isn't configured.
"""
settings.init()
logs.how_to_configure_alias(shell.how_to_configure())
def main(): def main():
parser = ArgumentParser(prog='thefuck') parser = ArgumentParser(prog='thefuck')
version = get_installation_info().version version = get_installation_info().version

86
thefuck/not_configured.py Normal file
View File

@ -0,0 +1,86 @@
# Initialize output before importing any module, that can use colorama.
from .system import init_output
init_output()
import os # noqa: E402
from psutil import Process # noqa: E402
from . import logs # noqa: E402
from .shells import shell # noqa: E402
from .conf import settings # noqa: E402
from .system import Path # noqa: E402
from .utils import get_cache_dir # noqa: E402
def _get_shell_pid():
"""Returns parent process pid."""
proc = Process(os.getpid())
try:
return proc.parent().pid
except TypeError:
return proc.parent.pid
def _get_not_configured_usage_tracker_path():
"""Returns path of special file where we store latest shell pid."""
return Path(get_cache_dir()).joinpath('thefuck.last_not_configured_run')
def _record_first_run():
"""Records shell pid to tracker file."""
with _get_not_configured_usage_tracker_path().open('w') as tracker:
tracker.write(str(_get_shell_pid()))
def _is_second_run():
"""Returns `True` when we know that `fuck` called second time."""
tracker_path = _get_not_configured_usage_tracker_path()
if not tracker_path.exists() or not shell.get_history()[-1] == 'fuck':
return False
current_pid = _get_shell_pid()
with tracker_path.open('r') as tracker:
return tracker.read() == str(current_pid)
def _is_already_configured(configuration_details):
"""Returns `True` when alias already in shell config."""
path = Path(configuration_details.path).expanduser()
with path.open('r') as shell_config:
return configuration_details.content in shell_config.read()
def _configure(configuration_details):
"""Adds alias to shell config."""
path = Path(configuration_details.path).expanduser()
with path.open('a') as shell_config:
shell_config.write('\n')
shell_config.write(configuration_details.content)
shell_config.write('\n')
def main():
"""Shows useful information about how-to configure alias on a first run
and configure automatically on a second.
It'll be only visible when user type fuck and when alias isn't configured.
"""
settings.init()
configuration_details = shell.how_to_configure()
if (
configuration_details and
configuration_details.can_configure_automatically
):
if _is_already_configured(configuration_details):
logs.already_configured(configuration_details)
return
elif _is_second_run():
_configure(configuration_details)
logs.configured_successfully(configuration_details)
return
else:
_record_first_run()
logs.how_to_configure_alias(configuration_details)

View File

@ -45,8 +45,7 @@ class Bash(Generic):
else: else:
config = 'bash config' config = 'bash config'
return { return self._create_shell_configuration(
'content': 'eval $(thefuck --alias)', content='eval $(thefuck --alias)',
'path': config, path=config,
'reload': u'source {}'.format(config), reload=u'source {}'.format(config))
}

View File

@ -67,11 +67,10 @@ class Fish(Generic):
return u'; and '.join(commands) return u'; and '.join(commands)
def how_to_configure(self): def how_to_configure(self):
return { return self._create_shell_configuration(
'content': r"eval (thefuck --alias | tr '\n' ';')", content=r"eval (thefuck --alias | tr '\n' ';')",
'path': '~/.config/fish/config.fish', path='~/.config/fish/config.fish',
'reload': 'fish', reload='fish')
}
def put_to_history(self, command): def put_to_history(self, command):
try: try:

View File

@ -2,8 +2,14 @@ import io
import os import os
import shlex import shlex
import six import six
from collections import namedtuple
from ..utils import memoize from ..utils import memoize
from ..conf import settings from ..conf import settings
from ..system import Path
ShellConfiguration = namedtuple('ShellConfiguration', (
'content', 'path', 'reload', 'can_configure_automatically'))
class Generic(object): class Generic(object):
@ -116,3 +122,10 @@ class Generic(object):
'shift', 'shopt', 'source', 'suspend', 'test', 'times', 'trap', 'shift', 'shopt', 'source', 'suspend', 'test', 'times', 'trap',
'type', 'typeset', 'ulimit', 'umask', 'unalias', 'unset', 'type', 'typeset', 'ulimit', 'umask', 'unalias', 'unset',
'until', 'wait', 'while'] 'until', 'wait', 'while']
def _create_shell_configuration(self, content, path, reload):
return ShellConfiguration(
content=content,
path=path,
reload=reload,
can_configure_automatically=Path(path).expanduser().exists())

View File

@ -1,4 +1,4 @@
from .generic import Generic from .generic import Generic, ShellConfiguration
class Powershell(Generic): class Powershell(Generic):
@ -18,8 +18,8 @@ class Powershell(Generic):
return u' -and '.join('({0})'.format(c) for c in commands) return u' -and '.join('({0})'.format(c) for c in commands)
def how_to_configure(self): def how_to_configure(self):
return { return ShellConfiguration(
'content': 'iex "thefuck --alias"', content='iex "thefuck --alias"',
'path': '$profile', path='$profile',
'reload': '& $profile', reload='& $profile',
} can_configure_automatically=False)

View File

@ -31,8 +31,7 @@ class Tcsh(Generic):
return u'#+{}\n{}\n'.format(int(time()), command_script) return u'#+{}\n{}\n'.format(int(time()), command_script)
def how_to_configure(self): def how_to_configure(self):
return { return self._create_shell_configuration(
'content': 'eval `thefuck --alias`', content='eval `thefuck --alias`',
'path': '~/.tcshrc', path='~/.tcshrc',
'reload': 'tcsh', reload='tcsh')
}

View File

@ -45,8 +45,7 @@ class Zsh(Generic):
return '' return ''
def how_to_configure(self): def how_to_configure(self):
return { return self._create_shell_configuration(
'content': 'eval $(thefuck --alias)', content='eval $(thefuck --alias)',
'path': '~/.zshrc', path='~/.zshrc',
'reload': 'source ~/.zshrc', reload='source ~/.zshrc')
}

View File

@ -178,6 +178,21 @@ def for_app(*app_names, **kwargs):
return decorator(_for_app) return decorator(_for_app)
def get_cache_dir():
default_xdg_cache_dir = os.path.expanduser("~/.cache")
cache_dir = os.getenv("XDG_CACHE_HOME", default_xdg_cache_dir)
# Ensure the cache_path exists, Python 2 does not have the exist_ok
# parameter
try:
os.makedirs(cache_dir)
except OSError:
if not os.path.isdir(cache_dir):
raise
return cache_dir
def cache(*depends_on): def cache(*depends_on):
"""Caches function result in temporary file. """Caches function result in temporary file.
@ -194,21 +209,6 @@ def cache(*depends_on):
except OSError: except OSError:
return '0' return '0'
def _get_cache_path():
default_xdg_cache_dir = os.path.expanduser("~/.cache")
cache_dir = os.getenv("XDG_CACHE_HOME", default_xdg_cache_dir)
cache_path = Path(cache_dir).joinpath('thefuck').as_posix()
# Ensure the cache_path exists, Python 2 does not have the exist_ok
# parameter
try:
os.makedirs(cache_dir)
except OSError:
if not os.path.isdir(cache_dir):
raise
return cache_path
@decorator @decorator
def _cache(fn, *args, **kwargs): def _cache(fn, *args, **kwargs):
if cache.disabled: if cache.disabled:
@ -219,7 +219,8 @@ def cache(*depends_on):
key = '{}.{}'.format(fn.__module__, repr(fn).split('at')[0]) key = '{}.{}'.format(fn.__module__, repr(fn).split('at')[0])
etag = '.'.join(_get_mtime(name) for name in depends_on) etag = '.'.join(_get_mtime(name) for name in depends_on)
cache_path = _get_cache_path() cache_dir = get_cache_dir()
cache_path = Path(cache_dir).joinpath('thefuck').as_posix()
try: try:
with closing(shelve.open(cache_path)) as db: with closing(shelve.open(cache_path)) as db: