1
0
mirror of https://github.com/nvbn/thefuck.git synced 2024-10-05 18:31:10 +01: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:
Pablo Aguiar 2018-10-08 22:32:30 +02:00 committed by Vladimir Iakovlev
parent 5fd4f74701
commit 25142f81f8
25 changed files with 189 additions and 37 deletions

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,4 +1,4 @@
from difflib import get_close_matches
from thefuck.utils import get_close_matches
from thefuck.specific.git import git_support

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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