mirror of
https://github.com/nvbn/thefuck.git
synced 2025-01-18 12:06:04 +00:00
Some improvements (#846)
* #833: do not require sudo on TravisCI * #N/A: Add Python dev releases to TravisCI pipeline Inspired by Brett Cannon's advise [1]. 1: https://snarky.ca/how-to-use-your-project-travis-to-help-test-python-itself/ * #837: try and kill proc and its children * #N/A: show shell information on `thefuck --version` * #N/A: omit default arguments to get_close_matches * #842: add settings var to control number of close matches * #N/A: remove `n` from the list of `get_closest`'s args
This commit is contained in:
parent
5fd4f74701
commit
25142f81f8
7
.github/ISSUE_TEMPLATE.md
vendored
7
.github/ISSUE_TEMPLATE.md
vendored
@ -6,11 +6,8 @@ update The Fuck and see if the bug is still there. -->
|
||||
if not, just open an issue on [GitHub](https://github.com/nvbn/thefuck) with
|
||||
the following basic information: -->
|
||||
|
||||
The output of `thefuck --version` (something like `The Fuck 3.1 using Python 3.5.0`):
|
||||
|
||||
FILL THIS IN
|
||||
|
||||
Your shell and its version (`bash`, `zsh`, *Windows PowerShell*, etc.):
|
||||
The output of `thefuck --version` (something like `The Fuck 3.1 using Python
|
||||
3.5.0 and Bash 4.4.12(1)-release`):
|
||||
|
||||
FILL THIS IN
|
||||
|
||||
|
18
.travis.yml
18
.travis.yml
@ -2,10 +2,21 @@ language: python
|
||||
sudo: false
|
||||
matrix:
|
||||
include:
|
||||
- os: linux
|
||||
dist: xenial
|
||||
python: "nightly"
|
||||
- os: linux
|
||||
dist: xenial
|
||||
python: "3.8-dev"
|
||||
- os: linux
|
||||
dist: xenial
|
||||
python: "3.7-dev"
|
||||
- os: linux
|
||||
dist: xenial
|
||||
python: "3.7"
|
||||
sudo: true
|
||||
- os: linux
|
||||
dist: trusty
|
||||
python: "3.6-dev"
|
||||
- os: linux
|
||||
dist: trusty
|
||||
python: "3.6"
|
||||
@ -20,6 +31,11 @@ matrix:
|
||||
python: "2.7"
|
||||
- os: osx
|
||||
language: generic
|
||||
allow_failures:
|
||||
- python: nightly
|
||||
- python: 3.8-dev
|
||||
- python: 3.7-dev
|
||||
- python: 3.6-dev
|
||||
services:
|
||||
- docker
|
||||
addons:
|
||||
|
@ -390,7 +390,8 @@ Several *The Fuck* parameters can be changed in the file `$XDG_CONFIG_HOME/thefu
|
||||
* `history_limit` – numeric value of how many history commands will be scanned, like `2000`;
|
||||
* `alter_history` – push fixed command to history, by default `True`;
|
||||
* `wait_slow_command` – max amount of time in seconds for getting previous command output if it in `slow_commands` list;
|
||||
* `slow_commands` – list of slow commands.
|
||||
* `slow_commands` – list of slow commands;
|
||||
* `num_close_matches` – maximum number of close matches to suggest, by default `3`.
|
||||
|
||||
An example of `settings.py`:
|
||||
|
||||
@ -405,6 +406,7 @@ debug = False
|
||||
history_limit = 9999
|
||||
wait_slow_command = 20
|
||||
slow_commands = ['react-native', 'gradle']
|
||||
num_close_matches = 5
|
||||
```
|
||||
|
||||
Or via environment variables:
|
||||
@ -420,7 +422,8 @@ rule with lower `priority` will be matched first;
|
||||
* `THEFUCK_HISTORY_LIMIT` – how many history commands will be scanned, like `2000`;
|
||||
* `THEFUCK_ALTER_HISTORY` – push fixed command to history `true/false`;
|
||||
* `THEFUCK_WAIT_SLOW_COMMAND` – max amount of time in seconds for getting previous command output if it in `slow_commands` list;
|
||||
* `THEFUCK_SLOW_COMMANDS` – list of slow commands, like `lein:gradle`.
|
||||
* `THEFUCK_SLOW_COMMANDS` – list of slow commands, like `lein:gradle`;
|
||||
* `THEFUCK_NUM_CLOSE_MATCHES` – maximum number of close matches to suggest, like `5`.
|
||||
|
||||
For example:
|
||||
|
||||
@ -432,6 +435,7 @@ export THEFUCK_WAIT_COMMAND=10
|
||||
export THEFUCK_NO_COLORS='false'
|
||||
export THEFUCK_PRIORITY='no_command=9999:apt_get=100'
|
||||
export THEFUCK_HISTORY_LIMIT='2000'
|
||||
export THEFUCK_NUM_CLOSE_MATCHES='5'
|
||||
```
|
||||
|
||||
## Third-party packages with rules
|
||||
|
58
tests/output_readers/test_rerun.py
Normal file
58
tests/output_readers/test_rerun.py
Normal file
@ -0,0 +1,58 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
|
||||
from mock import Mock, patch
|
||||
from psutil import AccessDenied, TimeoutExpired
|
||||
|
||||
from thefuck.output_readers import rerun
|
||||
|
||||
|
||||
class TestRerun(object):
|
||||
def setup_method(self, test_method):
|
||||
self.patcher = patch('thefuck.output_readers.rerun.Process')
|
||||
process_mock = self.patcher.start()
|
||||
self.proc_mock = process_mock.return_value = Mock()
|
||||
|
||||
def teardown_method(self, test_method):
|
||||
self.patcher.stop()
|
||||
|
||||
@patch('thefuck.output_readers.rerun._wait_output', return_value=False)
|
||||
@patch('thefuck.output_readers.rerun.Popen')
|
||||
def test_get_output(self, popen_mock, wait_output_mock):
|
||||
popen_mock.return_value.stdout.read.return_value = b'output'
|
||||
assert rerun.get_output('', '') is None
|
||||
wait_output_mock.assert_called_once()
|
||||
|
||||
def test_wait_output_is_slow(self, settings):
|
||||
assert rerun._wait_output(Mock(), True)
|
||||
self.proc_mock.wait.assert_called_once_with(settings.wait_slow_command)
|
||||
|
||||
def test_wait_output_is_not_slow(self, settings):
|
||||
assert rerun._wait_output(Mock(), False)
|
||||
self.proc_mock.wait.assert_called_once_with(settings.wait_command)
|
||||
|
||||
@patch('thefuck.output_readers.rerun._kill_process')
|
||||
def test_wait_output_timeout(self, kill_process_mock):
|
||||
self.proc_mock.wait.side_effect = TimeoutExpired(3)
|
||||
self.proc_mock.children.return_value = []
|
||||
assert not rerun._wait_output(Mock(), False)
|
||||
kill_process_mock.assert_called_once_with(self.proc_mock)
|
||||
|
||||
@patch('thefuck.output_readers.rerun._kill_process')
|
||||
def test_wait_output_timeout_children(self, kill_process_mock):
|
||||
self.proc_mock.wait.side_effect = TimeoutExpired(3)
|
||||
self.proc_mock.children.return_value = [Mock()] * 2
|
||||
assert not rerun._wait_output(Mock(), False)
|
||||
assert kill_process_mock.call_count == 3
|
||||
|
||||
def test_kill_process(self):
|
||||
proc = Mock()
|
||||
rerun._kill_process(proc)
|
||||
proc.kill.assert_called_once_with()
|
||||
|
||||
@patch('thefuck.output_readers.rerun.logs')
|
||||
def test_kill_process_access_denied(self, logs_mock):
|
||||
proc = Mock()
|
||||
proc.kill.side_effect = AccessDenied()
|
||||
rerun._kill_process(proc)
|
||||
proc.kill.assert_called_once_with()
|
||||
logs_mock.debug.assert_called_once()
|
@ -73,3 +73,8 @@ class TestBash(object):
|
||||
config_exists):
|
||||
config_exists.return_value = False
|
||||
assert not shell.how_to_configure().can_configure_automatically
|
||||
|
||||
def test_info(self, shell, mocker):
|
||||
patch = mocker.patch('thefuck.shells.bash.Popen')
|
||||
patch.return_value.stdout.read.side_effect = [b'3.5.9']
|
||||
assert shell.info() == 'Bash 3.5.9'
|
||||
|
@ -112,3 +112,7 @@ class TestFish(object):
|
||||
config_exists):
|
||||
config_exists.return_value = False
|
||||
assert not shell.how_to_configure().can_configure_automatically
|
||||
|
||||
def test_info(self, shell, Popen):
|
||||
Popen.return_value.stdout.read.side_effect = [b'3.5.9']
|
||||
assert shell.info() == 'Fish Shell 3.5.9'
|
||||
|
@ -68,3 +68,8 @@ class TestZsh(object):
|
||||
config_exists):
|
||||
config_exists.return_value = False
|
||||
assert not shell.how_to_configure().can_configure_automatically
|
||||
|
||||
def test_info(self, shell, mocker):
|
||||
patch = mocker.patch('thefuck.shells.zsh.Popen')
|
||||
patch.return_value.stdout.read.side_effect = [b'3.5.9']
|
||||
assert shell.info() == 'ZSH 3.5.9'
|
||||
|
@ -53,7 +53,8 @@ class TestSettingsFromEnv(object):
|
||||
'THEFUCK_NO_COLORS': 'false',
|
||||
'THEFUCK_PRIORITY': 'bash=10:lisp=wrong:vim=15',
|
||||
'THEFUCK_WAIT_SLOW_COMMAND': '999',
|
||||
'THEFUCK_SLOW_COMMANDS': 'lein:react-native:./gradlew'})
|
||||
'THEFUCK_SLOW_COMMANDS': 'lein:react-native:./gradlew',
|
||||
'THEFUCK_NUM_CLOSE_MATCHES': '359'})
|
||||
settings.init()
|
||||
assert settings.rules == ['bash', 'lisp']
|
||||
assert settings.exclude_rules == ['git', 'vim']
|
||||
@ -63,6 +64,7 @@ class TestSettingsFromEnv(object):
|
||||
assert settings.priority == {'bash': 10, 'vim': 15}
|
||||
assert settings.wait_slow_command == 999
|
||||
assert settings.slow_commands == ['lein', 'react-native', './gradlew']
|
||||
assert settings.num_close_matches == 359
|
||||
|
||||
def test_from_env_with_DEFAULT(self, os_environ, settings):
|
||||
os_environ.update({'THEFUCK_RULES': 'DEFAULT_RULES:bash:lisp'})
|
||||
|
@ -2,11 +2,11 @@
|
||||
|
||||
import pytest
|
||||
import warnings
|
||||
from mock import Mock
|
||||
from mock import Mock, patch
|
||||
from thefuck.utils import default_settings, \
|
||||
memoize, get_closest, get_all_executables, replace_argument, \
|
||||
get_all_matched_commands, is_app, for_app, cache, \
|
||||
get_valid_history_without_current, _cache
|
||||
get_valid_history_without_current, _cache, get_close_matches
|
||||
from thefuck.types import Command
|
||||
|
||||
|
||||
@ -50,6 +50,18 @@ class TestGetClosest(object):
|
||||
fallback_to_first=False) is None
|
||||
|
||||
|
||||
class TestGetCloseMatches(object):
|
||||
@patch('thefuck.utils.difflib_get_close_matches')
|
||||
def test_call_with_n(self, difflib_mock):
|
||||
get_close_matches('', [], 1)
|
||||
assert difflib_mock.call_args[0][2] == 1
|
||||
|
||||
@patch('thefuck.utils.difflib_get_close_matches')
|
||||
def test_call_without_n(self, difflib_mock, settings):
|
||||
get_close_matches('', [])
|
||||
assert difflib_mock.call_args[0][2] == settings.get('num_close_matches')
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def get_aliases(mocker):
|
||||
mocker.patch('thefuck.shells.shell.get_aliases',
|
||||
|
@ -95,7 +95,8 @@ class Settings(dict):
|
||||
return self._rules_from_env(val)
|
||||
elif attr == 'priority':
|
||||
return dict(self._priority_from_env(val))
|
||||
elif attr in ('wait_command', 'history_limit', 'wait_slow_command'):
|
||||
elif attr in ('wait_command', 'history_limit', 'wait_slow_command',
|
||||
'num_close_matches'):
|
||||
return int(val)
|
||||
elif attr in ('require_confirmation', 'no_colors', 'debug',
|
||||
'alter_history', 'instant_mode'):
|
||||
|
@ -42,6 +42,7 @@ DEFAULT_SETTINGS = {'rules': DEFAULT_RULES,
|
||||
'./gradlew', 'vagrant'],
|
||||
'repeat': False,
|
||||
'instant_mode': False,
|
||||
'num_close_matches': 3,
|
||||
'env': {'LC_ALL': 'C', 'LANG': 'C', 'GIT_TRACE': '1'}}
|
||||
|
||||
ENV_TO_ATTR = {'THEFUCK_RULES': 'rules',
|
||||
@ -56,7 +57,8 @@ ENV_TO_ATTR = {'THEFUCK_RULES': 'rules',
|
||||
'THEFUCK_WAIT_SLOW_COMMAND': 'wait_slow_command',
|
||||
'THEFUCK_SLOW_COMMANDS': 'slow_commands',
|
||||
'THEFUCK_REPEAT': 'repeat',
|
||||
'THEFUCK_INSTANT_MODE': 'instant_mode'}
|
||||
'THEFUCK_INSTANT_MODE': 'instant_mode',
|
||||
'THEFUCK_NUM_CLOSE_MATCHES': 'num_close_matches'}
|
||||
|
||||
SETTINGS_HEADER = u"""# The Fuck settings file
|
||||
#
|
||||
|
@ -8,6 +8,7 @@ import sys # noqa: E402
|
||||
from .. import logs # noqa: E402
|
||||
from ..argument_parser import Parser # noqa: E402
|
||||
from ..utils import get_installation_info # noqa: E402
|
||||
from ..shells import shell # noqa: E402
|
||||
from .alias import print_alias # noqa: E402
|
||||
from .fix_command import fix_command # noqa: E402
|
||||
|
||||
@ -20,7 +21,7 @@ def main():
|
||||
parser.print_help()
|
||||
elif known_args.version:
|
||||
logs.version(get_installation_info().version,
|
||||
sys.version.split()[0])
|
||||
sys.version.split()[0], shell.info())
|
||||
elif known_args.command or 'TF_HISTORY' in os.environ:
|
||||
fix_command(known_args)
|
||||
elif known_args.alias:
|
||||
|
@ -134,7 +134,8 @@ def configured_successfully(configuration_details):
|
||||
reload=configuration_details.reload))
|
||||
|
||||
|
||||
def version(thefuck_version, python_version):
|
||||
def version(thefuck_version, python_version, shell_info):
|
||||
sys.stderr.write(
|
||||
u'The Fuck {} using Python {}\n'.format(thefuck_version,
|
||||
python_version))
|
||||
u'The Fuck {} using Python {} and {}\n'.format(thefuck_version,
|
||||
python_version,
|
||||
shell_info))
|
||||
|
@ -1,11 +1,25 @@
|
||||
import os
|
||||
import shlex
|
||||
from subprocess import Popen, PIPE, STDOUT
|
||||
from psutil import Process, TimeoutExpired
|
||||
from psutil import AccessDenied, Process, TimeoutExpired
|
||||
from .. import logs
|
||||
from ..conf import settings
|
||||
|
||||
|
||||
def _kill_process(proc):
|
||||
"""Tries to kill the process otherwise just logs a debug message, the
|
||||
process will be killed when thefuck terminates.
|
||||
|
||||
:type proc: Process
|
||||
|
||||
"""
|
||||
try:
|
||||
proc.kill()
|
||||
except AccessDenied:
|
||||
logs.debug(u'Rerun: process PID {} ({}) could not be terminated'.format(
|
||||
proc.pid, proc.exe()))
|
||||
|
||||
|
||||
def _wait_output(popen, is_slow):
|
||||
"""Returns `True` if we can get output of the command in the
|
||||
`settings.wait_command` time.
|
||||
@ -23,8 +37,8 @@ def _wait_output(popen, is_slow):
|
||||
return True
|
||||
except TimeoutExpired:
|
||||
for child in proc.children(recursive=True):
|
||||
child.kill()
|
||||
proc.kill()
|
||||
_kill_process(child)
|
||||
_kill_process(proc)
|
||||
return False
|
||||
|
||||
|
||||
|
@ -20,7 +20,7 @@ def _get_formulas():
|
||||
|
||||
|
||||
def _get_similar_formula(formula_name):
|
||||
return get_closest(formula_name, _get_formulas(), 1, 0.85)
|
||||
return get_closest(formula_name, _get_formulas(), cutoff=0.85)
|
||||
|
||||
|
||||
def match(command):
|
||||
|
@ -2,10 +2,9 @@
|
||||
|
||||
import os
|
||||
import six
|
||||
from difflib import get_close_matches
|
||||
from thefuck.specific.sudo import sudo_support
|
||||
from thefuck.rules import cd_mkdir
|
||||
from thefuck.utils import for_app
|
||||
from thefuck.utils import for_app, get_close_matches
|
||||
|
||||
__author__ = "mmussomele"
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
from difflib import get_close_matches
|
||||
from thefuck.utils import get_close_matches
|
||||
from thefuck.specific.git import git_support
|
||||
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
from difflib import get_close_matches
|
||||
from thefuck.utils import get_closest, get_valid_history_without_current
|
||||
from thefuck.utils import get_close_matches, get_closest, \
|
||||
get_valid_history_without_current
|
||||
|
||||
|
||||
def match(command):
|
||||
|
@ -1,5 +1,4 @@
|
||||
from thefuck.utils import replace_command, for_app
|
||||
from difflib import get_close_matches
|
||||
from thefuck.utils import for_app, get_close_matches, replace_command
|
||||
import re
|
||||
|
||||
|
||||
@ -25,8 +24,7 @@ def get_new_command(command):
|
||||
available_lifecycles = _getavailable_lifecycles(command)
|
||||
if available_lifecycles and failed_lifecycle:
|
||||
selected_lifecycle = get_close_matches(
|
||||
failed_lifecycle.group(1), available_lifecycles.group(1).split(", "),
|
||||
3, 0.6)
|
||||
failed_lifecycle.group(1), available_lifecycles.group(1).split(", "))
|
||||
return replace_command(command, failed_lifecycle.group(1), selected_lifecycle)
|
||||
else:
|
||||
return []
|
||||
|
@ -1,5 +1,4 @@
|
||||
from difflib import get_close_matches
|
||||
from thefuck.utils import get_all_executables, \
|
||||
from thefuck.utils import get_all_executables, get_close_matches, \
|
||||
get_valid_history_without_current, get_closest, which
|
||||
from thefuck.specific.sudo import sudo_support
|
||||
|
||||
|
@ -1,9 +1,10 @@
|
||||
import os
|
||||
from subprocess import Popen, PIPE
|
||||
from tempfile import gettempdir
|
||||
from uuid import uuid4
|
||||
from ..conf import settings
|
||||
from ..const import ARGUMENT_PLACEHOLDER, USER_COMMAND_MARK
|
||||
from ..utils import memoize
|
||||
from ..utils import DEVNULL, memoize
|
||||
from .generic import Generic
|
||||
|
||||
|
||||
@ -81,3 +82,10 @@ class Bash(Generic):
|
||||
content=u'eval $(thefuck --alias)',
|
||||
path=config,
|
||||
reload=u'source {}'.format(config))
|
||||
|
||||
def info(self):
|
||||
"""Returns the name and version of the current shell"""
|
||||
proc = Popen(['bash', '-c', 'echo $BASH_VERSION'],
|
||||
stdout=PIPE, stderr=DEVNULL)
|
||||
version = proc.stdout.read().decode('utf-8').strip()
|
||||
return u'Bash {}'.format(version)
|
||||
|
@ -103,6 +103,13 @@ class Fish(Generic):
|
||||
path='~/.config/fish/config.fish',
|
||||
reload='fish')
|
||||
|
||||
def info(self):
|
||||
"""Returns the name and version of the current shell"""
|
||||
proc = Popen(['fish', '-c', 'echo $FISH_VERSION'],
|
||||
stdout=PIPE, stderr=DEVNULL)
|
||||
version = proc.stdout.read().decode('utf-8').strip()
|
||||
return u'Fish Shell {}'.format(version)
|
||||
|
||||
def put_to_history(self, command):
|
||||
try:
|
||||
return self._put_to_history(command)
|
||||
|
@ -131,6 +131,10 @@ class Generic(object):
|
||||
'type', 'typeset', 'ulimit', 'umask', 'unalias', 'unset',
|
||||
'until', 'wait', 'while']
|
||||
|
||||
def info(self):
|
||||
"""Returns the name and version of the current shell"""
|
||||
return 'Generic Shell'
|
||||
|
||||
def _create_shell_configuration(self, content, path, reload):
|
||||
return ShellConfiguration(
|
||||
content=content,
|
||||
|
@ -1,10 +1,11 @@
|
||||
from time import time
|
||||
import os
|
||||
from subprocess import Popen, PIPE
|
||||
from tempfile import gettempdir
|
||||
from uuid import uuid4
|
||||
from ..conf import settings
|
||||
from ..const import ARGUMENT_PLACEHOLDER, USER_COMMAND_MARK
|
||||
from ..utils import memoize
|
||||
from ..utils import DEVNULL, memoize
|
||||
from .generic import Generic
|
||||
|
||||
|
||||
@ -85,3 +86,10 @@ class Zsh(Generic):
|
||||
content=u'eval $(thefuck --alias)',
|
||||
path='~/.zshrc',
|
||||
reload='source ~/.zshrc')
|
||||
|
||||
def info(self):
|
||||
"""Returns the name and version of the current shell"""
|
||||
proc = Popen(['zsh', '-c', 'echo $ZSH_VERSION'],
|
||||
stdout=PIPE, stderr=DEVNULL)
|
||||
version = proc.stdout.read().decode('utf-8').strip()
|
||||
return u'ZSH {}'.format(version)
|
||||
|
@ -5,7 +5,7 @@ import re
|
||||
import shelve
|
||||
import six
|
||||
from decorator import decorator
|
||||
from difflib import get_close_matches
|
||||
from difflib import get_close_matches as difflib_get_close_matches
|
||||
from functools import wraps
|
||||
from .logs import warn
|
||||
from .conf import settings
|
||||
@ -86,16 +86,23 @@ def default_settings(params):
|
||||
return decorator(_default_settings)
|
||||
|
||||
|
||||
def get_closest(word, possibilities, n=3, cutoff=0.6, fallback_to_first=True):
|
||||
def get_closest(word, possibilities, cutoff=0.6, fallback_to_first=True):
|
||||
"""Returns closest match or just first from possibilities."""
|
||||
possibilities = list(possibilities)
|
||||
try:
|
||||
return get_close_matches(word, possibilities, n, cutoff)[0]
|
||||
return difflib_get_close_matches(word, possibilities, 1, cutoff)[0]
|
||||
except IndexError:
|
||||
if fallback_to_first:
|
||||
return possibilities[0]
|
||||
|
||||
|
||||
def get_close_matches(word, possibilities, n=None, cutoff=0.6):
|
||||
"""Overrides `difflib.get_close_match` to controle argument `n`."""
|
||||
if n is None:
|
||||
n = settings.num_close_matches
|
||||
return difflib_get_close_matches(word, possibilities, n, cutoff)
|
||||
|
||||
|
||||
@memoize
|
||||
def get_all_executables():
|
||||
from thefuck.shells import shell
|
||||
|
Loading…
x
Reference in New Issue
Block a user