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 if not, just open an issue on [GitHub](https://github.com/nvbn/thefuck) with
the following basic information: --> the following basic information: -->
The output of `thefuck --version` (something like `The Fuck 3.1 using Python 3.5.0`): 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
Your shell and its version (`bash`, `zsh`, *Windows PowerShell*, etc.):
FILL THIS IN FILL THIS IN

View File

@ -2,10 +2,21 @@ language: python
sudo: false sudo: false
matrix: matrix:
include: 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 - os: linux
dist: xenial dist: xenial
python: "3.7" python: "3.7"
sudo: true - os: linux
dist: trusty
python: "3.6-dev"
- os: linux - os: linux
dist: trusty dist: trusty
python: "3.6" python: "3.6"
@ -20,6 +31,11 @@ matrix:
python: "2.7" python: "2.7"
- os: osx - os: osx
language: generic language: generic
allow_failures:
- python: nightly
- python: 3.8-dev
- python: 3.7-dev
- python: 3.6-dev
services: services:
- docker - docker
addons: 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`; * `history_limit` – numeric value of how many history commands will be scanned, like `2000`;
* `alter_history` – push fixed command to history, by default `True`; * `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; * `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`: An example of `settings.py`:
@ -405,6 +406,7 @@ debug = False
history_limit = 9999 history_limit = 9999
wait_slow_command = 20 wait_slow_command = 20
slow_commands = ['react-native', 'gradle'] slow_commands = ['react-native', 'gradle']
num_close_matches = 5
``` ```
Or via environment variables: 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_HISTORY_LIMIT` – how many history commands will be scanned, like `2000`;
* `THEFUCK_ALTER_HISTORY` – push fixed command to history `true/false`; * `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_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: For example:
@ -432,6 +435,7 @@ export THEFUCK_WAIT_COMMAND=10
export THEFUCK_NO_COLORS='false' export THEFUCK_NO_COLORS='false'
export THEFUCK_PRIORITY='no_command=9999:apt_get=100' export THEFUCK_PRIORITY='no_command=9999:apt_get=100'
export THEFUCK_HISTORY_LIMIT='2000' export THEFUCK_HISTORY_LIMIT='2000'
export THEFUCK_NUM_CLOSE_MATCHES='5'
``` ```
## Third-party packages with rules ## 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):
config_exists.return_value = False config_exists.return_value = False
assert not shell.how_to_configure().can_configure_automatically 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):
config_exists.return_value = False config_exists.return_value = False
assert not shell.how_to_configure().can_configure_automatically 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):
config_exists.return_value = False config_exists.return_value = False
assert not shell.how_to_configure().can_configure_automatically 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_NO_COLORS': 'false',
'THEFUCK_PRIORITY': 'bash=10:lisp=wrong:vim=15', 'THEFUCK_PRIORITY': 'bash=10:lisp=wrong:vim=15',
'THEFUCK_WAIT_SLOW_COMMAND': '999', '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() settings.init()
assert settings.rules == ['bash', 'lisp'] assert settings.rules == ['bash', 'lisp']
assert settings.exclude_rules == ['git', 'vim'] assert settings.exclude_rules == ['git', 'vim']
@ -63,6 +64,7 @@ class TestSettingsFromEnv(object):
assert settings.priority == {'bash': 10, 'vim': 15} assert settings.priority == {'bash': 10, 'vim': 15}
assert settings.wait_slow_command == 999 assert settings.wait_slow_command == 999
assert settings.slow_commands == ['lein', 'react-native', './gradlew'] assert settings.slow_commands == ['lein', 'react-native', './gradlew']
assert settings.num_close_matches == 359
def test_from_env_with_DEFAULT(self, os_environ, settings): def test_from_env_with_DEFAULT(self, os_environ, settings):
os_environ.update({'THEFUCK_RULES': 'DEFAULT_RULES:bash:lisp'}) os_environ.update({'THEFUCK_RULES': 'DEFAULT_RULES:bash:lisp'})

View File

@ -2,11 +2,11 @@
import pytest import pytest
import warnings import warnings
from mock import Mock from mock import Mock, patch
from thefuck.utils import default_settings, \ from thefuck.utils import default_settings, \
memoize, get_closest, get_all_executables, replace_argument, \ memoize, get_closest, get_all_executables, replace_argument, \
get_all_matched_commands, is_app, for_app, cache, \ 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 from thefuck.types import Command
@ -50,6 +50,18 @@ class TestGetClosest(object):
fallback_to_first=False) is None 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 @pytest.fixture
def get_aliases(mocker): def get_aliases(mocker):
mocker.patch('thefuck.shells.shell.get_aliases', mocker.patch('thefuck.shells.shell.get_aliases',

View File

@ -95,7 +95,8 @@ class Settings(dict):
return self._rules_from_env(val) return self._rules_from_env(val)
elif attr == 'priority': elif attr == 'priority':
return dict(self._priority_from_env(val)) 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) return int(val)
elif attr in ('require_confirmation', 'no_colors', 'debug', elif attr in ('require_confirmation', 'no_colors', 'debug',
'alter_history', 'instant_mode'): 'alter_history', 'instant_mode'):

View File

@ -42,6 +42,7 @@ DEFAULT_SETTINGS = {'rules': DEFAULT_RULES,
'./gradlew', 'vagrant'], './gradlew', 'vagrant'],
'repeat': False, 'repeat': False,
'instant_mode': False, 'instant_mode': False,
'num_close_matches': 3,
'env': {'LC_ALL': 'C', 'LANG': 'C', 'GIT_TRACE': '1'}} 'env': {'LC_ALL': 'C', 'LANG': 'C', 'GIT_TRACE': '1'}}
ENV_TO_ATTR = {'THEFUCK_RULES': 'rules', ENV_TO_ATTR = {'THEFUCK_RULES': 'rules',
@ -56,7 +57,8 @@ ENV_TO_ATTR = {'THEFUCK_RULES': 'rules',
'THEFUCK_WAIT_SLOW_COMMAND': 'wait_slow_command', 'THEFUCK_WAIT_SLOW_COMMAND': 'wait_slow_command',
'THEFUCK_SLOW_COMMANDS': 'slow_commands', 'THEFUCK_SLOW_COMMANDS': 'slow_commands',
'THEFUCK_REPEAT': 'repeat', '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 SETTINGS_HEADER = u"""# The Fuck settings file
# #

View File

@ -8,6 +8,7 @@ import sys # noqa: E402
from .. import logs # noqa: E402 from .. import logs # noqa: E402
from ..argument_parser import Parser # noqa: E402 from ..argument_parser import Parser # noqa: E402
from ..utils import get_installation_info # noqa: E402 from ..utils import get_installation_info # noqa: E402
from ..shells import shell # noqa: E402
from .alias import print_alias # noqa: E402 from .alias import print_alias # noqa: E402
from .fix_command import fix_command # noqa: E402 from .fix_command import fix_command # noqa: E402
@ -20,7 +21,7 @@ def main():
parser.print_help() parser.print_help()
elif known_args.version: elif known_args.version:
logs.version(get_installation_info().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: elif known_args.command or 'TF_HISTORY' in os.environ:
fix_command(known_args) fix_command(known_args)
elif known_args.alias: elif known_args.alias:

View File

@ -134,7 +134,8 @@ def configured_successfully(configuration_details):
reload=configuration_details.reload)) reload=configuration_details.reload))
def version(thefuck_version, python_version): def version(thefuck_version, python_version, shell_info):
sys.stderr.write( sys.stderr.write(
u'The Fuck {} using Python {}\n'.format(thefuck_version, u'The Fuck {} using Python {} and {}\n'.format(thefuck_version,
python_version)) python_version,
shell_info))

View File

@ -1,11 +1,25 @@
import os import os
import shlex import shlex
from subprocess import Popen, PIPE, STDOUT from subprocess import Popen, PIPE, STDOUT
from psutil import Process, TimeoutExpired from psutil import AccessDenied, Process, TimeoutExpired
from .. import logs from .. import logs
from ..conf import settings 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): def _wait_output(popen, is_slow):
"""Returns `True` if we can get output of the command in the """Returns `True` if we can get output of the command in the
`settings.wait_command` time. `settings.wait_command` time.
@ -23,8 +37,8 @@ def _wait_output(popen, is_slow):
return True return True
except TimeoutExpired: except TimeoutExpired:
for child in proc.children(recursive=True): for child in proc.children(recursive=True):
child.kill() _kill_process(child)
proc.kill() _kill_process(proc)
return False return False

View File

@ -20,7 +20,7 @@ def _get_formulas():
def _get_similar_formula(formula_name): 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): def match(command):

View File

@ -2,10 +2,9 @@
import os import os
import six import six
from difflib import get_close_matches
from thefuck.specific.sudo import sudo_support from thefuck.specific.sudo import sudo_support
from thefuck.rules import cd_mkdir from thefuck.rules import cd_mkdir
from thefuck.utils import for_app from thefuck.utils import for_app, get_close_matches
__author__ = "mmussomele" __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 from thefuck.specific.git import git_support

View File

@ -1,5 +1,5 @@
from difflib import get_close_matches from thefuck.utils import get_close_matches, get_closest, \
from thefuck.utils import get_closest, get_valid_history_without_current get_valid_history_without_current
def match(command): def match(command):

View File

@ -1,5 +1,4 @@
from thefuck.utils import replace_command, for_app from thefuck.utils import for_app, get_close_matches, replace_command
from difflib import get_close_matches
import re import re
@ -25,8 +24,7 @@ def get_new_command(command):
available_lifecycles = _getavailable_lifecycles(command) available_lifecycles = _getavailable_lifecycles(command)
if available_lifecycles and failed_lifecycle: if available_lifecycles and failed_lifecycle:
selected_lifecycle = get_close_matches( selected_lifecycle = get_close_matches(
failed_lifecycle.group(1), available_lifecycles.group(1).split(", "), failed_lifecycle.group(1), available_lifecycles.group(1).split(", "))
3, 0.6)
return replace_command(command, failed_lifecycle.group(1), selected_lifecycle) return replace_command(command, failed_lifecycle.group(1), selected_lifecycle)
else: else:
return [] return []

View File

@ -1,5 +1,4 @@
from difflib import get_close_matches from thefuck.utils import get_all_executables, get_close_matches, \
from thefuck.utils import get_all_executables, \
get_valid_history_without_current, get_closest, which get_valid_history_without_current, get_closest, which
from thefuck.specific.sudo import sudo_support from thefuck.specific.sudo import sudo_support

View File

@ -1,9 +1,10 @@
import os import os
from subprocess import Popen, PIPE
from tempfile import gettempdir from tempfile import gettempdir
from uuid import uuid4 from uuid import uuid4
from ..conf import settings from ..conf import settings
from ..const import ARGUMENT_PLACEHOLDER, USER_COMMAND_MARK from ..const import ARGUMENT_PLACEHOLDER, USER_COMMAND_MARK
from ..utils import memoize from ..utils import DEVNULL, memoize
from .generic import Generic from .generic import Generic
@ -81,3 +82,10 @@ class Bash(Generic):
content=u'eval $(thefuck --alias)', content=u'eval $(thefuck --alias)',
path=config, path=config,
reload=u'source {}'.format(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', path='~/.config/fish/config.fish',
reload='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): def put_to_history(self, command):
try: try:
return self._put_to_history(command) return self._put_to_history(command)

View File

@ -131,6 +131,10 @@ class Generic(object):
'type', 'typeset', 'ulimit', 'umask', 'unalias', 'unset', 'type', 'typeset', 'ulimit', 'umask', 'unalias', 'unset',
'until', 'wait', 'while'] '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): def _create_shell_configuration(self, content, path, reload):
return ShellConfiguration( return ShellConfiguration(
content=content, content=content,

View File

@ -1,10 +1,11 @@
from time import time from time import time
import os import os
from subprocess import Popen, PIPE
from tempfile import gettempdir from tempfile import gettempdir
from uuid import uuid4 from uuid import uuid4
from ..conf import settings from ..conf import settings
from ..const import ARGUMENT_PLACEHOLDER, USER_COMMAND_MARK from ..const import ARGUMENT_PLACEHOLDER, USER_COMMAND_MARK
from ..utils import memoize from ..utils import DEVNULL, memoize
from .generic import Generic from .generic import Generic
@ -85,3 +86,10 @@ class Zsh(Generic):
content=u'eval $(thefuck --alias)', content=u'eval $(thefuck --alias)',
path='~/.zshrc', path='~/.zshrc',
reload='source ~/.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 shelve
import six import six
from decorator import decorator 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 functools import wraps
from .logs import warn from .logs import warn
from .conf import settings from .conf import settings
@ -86,16 +86,23 @@ def default_settings(params):
return decorator(_default_settings) 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.""" """Returns closest match or just first from possibilities."""
possibilities = list(possibilities) possibilities = list(possibilities)
try: try:
return get_close_matches(word, possibilities, n, cutoff)[0] return difflib_get_close_matches(word, possibilities, 1, cutoff)[0]
except IndexError: except IndexError:
if fallback_to_first: if fallback_to_first:
return possibilities[0] 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 @memoize
def get_all_executables(): def get_all_executables():
from thefuck.shells import shell from thefuck.shells import shell