mirror of
https://github.com/nvbn/thefuck.git
synced 2025-03-14 06:38:32 +00:00
Merge branch 'master' of https://github.com/nvbn/thefuck into conda
This commit is contained in:
commit
9a91f6cc70
49
.github/workflows/test.yml
vendored
Normal file
49
.github/workflows/test.yml
vendored
Normal file
@ -0,0 +1,49 @@
|
||||
name: Tests
|
||||
|
||||
on: [push, pull_request]
|
||||
|
||||
env:
|
||||
PYTHON_LATEST: 3.9
|
||||
|
||||
jobs:
|
||||
test:
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||
python-version: [2.7, 3.5, 3.6, 3.7, 3.8, 3.9, 3.10-dev]
|
||||
fail-fast: false
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
- name: Cache dependencies
|
||||
id: cache-deps
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: |
|
||||
${{ env.pythonLocation }}/bin/*
|
||||
${{ env.pythonLocation }}/lib/*
|
||||
${{ env.pythonLocation }}/scripts/*
|
||||
key: ${{ runner.os }}-pip-${{ matrix.python-version }}-${{ hashFiles('setup.py', 'requirements.txt') }}
|
||||
- name: Install The Fuck with all dependencies
|
||||
if: steps.cache-deps.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
pip install -Ur requirements.txt coveralls
|
||||
python setup.py develop
|
||||
- name: Lint
|
||||
if: matrix.os == 'ubuntu-latest' && matrix.python-version == env.PYTHON_LATEST
|
||||
run: flake8
|
||||
- name: Run tests
|
||||
if: matrix.os != 'ubuntu-latest' || matrix.python-version != env.PYTHON_LATEST
|
||||
run: coverage run --source=thefuck,tests -m pytest -v --capture=sys tests
|
||||
- name: Run tests (including functional)
|
||||
if: matrix.os == 'ubuntu-latest' && matrix.python-version == env.PYTHON_LATEST
|
||||
run: coverage run --source=thefuck,tests -m pytest -v --capture=sys tests --enable-functional
|
||||
- name: Post coverage results
|
||||
if: matrix.os == 'ubuntu-latest' && matrix.python-version == env.PYTHON_LATEST
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: coveralls --service=github
|
51
.travis.yml
51
.travis.yml
@ -1,51 +0,0 @@
|
||||
language: python
|
||||
sudo: false
|
||||
os: linux
|
||||
dist: xenial
|
||||
matrix:
|
||||
include:
|
||||
- python: "nightly"
|
||||
- python: "3.8-dev"
|
||||
- python: "3.8"
|
||||
- python: "3.7-dev"
|
||||
- python: "3.7"
|
||||
- python: "3.6-dev"
|
||||
- python: "3.6"
|
||||
- python: "3.5"
|
||||
- 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:
|
||||
apt:
|
||||
packages:
|
||||
- python-commandnotfound
|
||||
- python3-commandnotfound
|
||||
before_install:
|
||||
- if [[ $TRAVIS_OS_NAME == "osx" ]]; then rm -rf /usr/local/include/c++; fi
|
||||
- if [[ $TRAVIS_OS_NAME == "osx" ]]; then brew update; fi
|
||||
- if [[ $TRAVIS_OS_NAME == "osx" ]]; then brew unlink python@2; fi
|
||||
- if [[ $TRAVIS_OS_NAME == "osx" ]]; then brew upgrade python; fi
|
||||
- if [[ $TRAVIS_OS_NAME == "osx" ]]; then pip3 install virtualenv; fi
|
||||
- if [[ $TRAVIS_OS_NAME == "osx" ]]; then virtualenv venv -p python3; fi
|
||||
- if [[ $TRAVIS_OS_NAME == "osx" ]]; then source venv/bin/activate; fi
|
||||
- pip install -U pip
|
||||
- pip install -U coveralls
|
||||
install:
|
||||
- pip install -Ur requirements.txt
|
||||
- python setup.py develop
|
||||
- rm -rf build
|
||||
script:
|
||||
- flake8
|
||||
- export COVERAGE_PYTHON_VERSION=python-${TRAVIS_PYTHON_VERSION:0:1}
|
||||
- export RUN_TESTS="coverage run --source=thefuck,tests -m py.test -v --capture=sys tests"
|
||||
- if [[ $TRAVIS_PYTHON_VERSION == 3.8 && $TRAVIS_OS_NAME != "osx" ]]; then $RUN_TESTS --enable-functional; fi
|
||||
- if [[ $TRAVIS_PYTHON_VERSION != 3.8 || $TRAVIS_OS_NAME == "osx" ]]; then $RUN_TESTS; fi
|
||||
after_success:
|
||||
- if [[ $TRAVIS_PYTHON_VERSION == 3.8 && $TRAVIS_OS_NAME != "osx" ]]; then coveralls; fi
|
@ -1,4 +1,4 @@
|
||||
# The Fuck [![Version][version-badge]][version-link] [![Build Status][travis-badge]][travis-link] [![Windows Build Status][appveyor-badge]][appveyor-link] [![Coverage][coverage-badge]][coverage-link] [![MIT License][license-badge]](LICENSE.md)
|
||||
# The Fuck [![Version][version-badge]][version-link] [![Build Status][workflow-badge]][workflow-link] [![Coverage][coverage-badge]][coverage-link] [![MIT License][license-badge]](LICENSE.md)
|
||||
|
||||
*The Fuck* is a magnificent app, inspired by a [@liamosaur](https://twitter.com/liamosaur/)
|
||||
[tweet](https://twitter.com/liamosaur/status/506975850596536320),
|
||||
@ -282,6 +282,7 @@ following rules are enabled by default:
|
||||
* `pyenv_no_such_command` – fixes wrong pyenv commands like `pyenv isntall` or `pyenv list`;
|
||||
* `python_command` – prepends `python` when you try to run non-executable/without `./` python script;
|
||||
* `python_execute` – appends missing `.py` when executing Python files;
|
||||
* `python_module_error` – fixes ModuleNotFoundError by trying to `pip install` that module;
|
||||
* `quotation_marks` – fixes uneven usage of `'` and `"` when containing args';
|
||||
* `path_from_history` – replaces not found path with similar absolute path from history;
|
||||
* `react_native_command_unrecognized` – fixes unrecognized `react-native` commands;
|
||||
@ -503,10 +504,8 @@ Project License can be found [here](LICENSE.md).
|
||||
|
||||
[version-badge]: https://img.shields.io/pypi/v/thefuck.svg?label=version
|
||||
[version-link]: https://pypi.python.org/pypi/thefuck/
|
||||
[travis-badge]: https://travis-ci.org/nvbn/thefuck.svg?branch=master
|
||||
[travis-link]: https://travis-ci.org/nvbn/thefuck
|
||||
[appveyor-badge]: https://ci.appveyor.com/api/projects/status/1sskj4imj02um0gu/branch/master?svg=true
|
||||
[appveyor-link]: https://ci.appveyor.com/project/nvbn/thefuck
|
||||
[workflow-badge]: https://github.com/divykj/thefuck/workflows/Tests/badge.svg
|
||||
[workflow-link]: https://github.com/divykj/thefuck/actions?query=workflow%3ATests
|
||||
[coverage-badge]: https://img.shields.io/coveralls/nvbn/thefuck.svg
|
||||
[coverage-link]: https://coveralls.io/github/nvbn/thefuck
|
||||
[license-badge]: https://img.shields.io/badge/license-MIT-007EC7.svg
|
||||
|
23
appveyor.yml
23
appveyor.yml
@ -1,23 +0,0 @@
|
||||
build: false
|
||||
|
||||
environment:
|
||||
matrix:
|
||||
- PYTHON: "C:/Python27"
|
||||
- PYTHON: "C:/Python35"
|
||||
- PYTHON: "C:/Python36"
|
||||
- PYTHON: "C:/Python37"
|
||||
|
||||
init:
|
||||
- "ECHO %PYTHON%"
|
||||
- ps: "ls C:/Python*"
|
||||
|
||||
install:
|
||||
- "curl -fsS -o C:/get-pip.py https://bootstrap.pypa.io/get-pip.py"
|
||||
- "%PYTHON%/python.exe C:/get-pip.py"
|
||||
- "%PYTHON%/Scripts/pip.exe install -U setuptools"
|
||||
- "%PYTHON%/python.exe setup.py develop"
|
||||
- "%PYTHON%/Scripts/pip.exe install -U -r requirements.txt"
|
||||
|
||||
test_script:
|
||||
- "%PYTHON%/python.exe -m flake8"
|
||||
- "%PYTHON%/Scripts/py.test.exe -sv"
|
@ -7,6 +7,10 @@ from thefuck.system import Path
|
||||
shells.shell = shells.Generic()
|
||||
|
||||
|
||||
def pytest_configure(config):
|
||||
config.addinivalue_line("markers", "functional: mark test as functional")
|
||||
|
||||
|
||||
def pytest_addoption(parser):
|
||||
"""Adds `--enable-functional` argument."""
|
||||
group = parser.getgroup("thefuck")
|
||||
|
@ -1,6 +1,7 @@
|
||||
import pytest
|
||||
from thefuck.rules.go_unknown_command import match, get_new_command
|
||||
from thefuck.types import Command
|
||||
from thefuck.utils import which
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@ -17,5 +18,6 @@ def test_not_match():
|
||||
assert not match(Command('go run', 'go run: no go files listed'))
|
||||
|
||||
|
||||
@pytest.mark.skipif(not which('go'), reason='Skip if go executable not found')
|
||||
def test_get_new_command(build_misspelled_output):
|
||||
assert get_new_command(Command('go bulid', build_misspelled_output)) == 'go build'
|
||||
|
63
tests/rules/test_python_module_error.py
Normal file
63
tests/rules/test_python_module_error.py
Normal file
@ -0,0 +1,63 @@
|
||||
import pytest
|
||||
|
||||
from thefuck.rules.python_module_error import get_new_command, match
|
||||
from thefuck.types import Command
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def module_error_output(filename, module_name):
|
||||
return """Traceback (most recent call last):
|
||||
File "{0}", line 1, in <module>
|
||||
import {1}
|
||||
ModuleNotFoundError: No module named '{1}'""".format(
|
||||
filename, module_name
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"test",
|
||||
[
|
||||
Command("python hello_world.py", "Hello World"),
|
||||
Command(
|
||||
"./hello_world.py",
|
||||
"""Traceback (most recent call last):
|
||||
File "hello_world.py", line 1, in <module>
|
||||
pritn("Hello World")
|
||||
NameError: name 'pritn' is not defined""",
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_not_match(test):
|
||||
assert not match(test)
|
||||
|
||||
|
||||
positive_tests = [
|
||||
(
|
||||
"python some_script.py",
|
||||
"some_script.py",
|
||||
"more_itertools",
|
||||
"pip install more_itertools && python some_script.py",
|
||||
),
|
||||
(
|
||||
"./some_other_script.py",
|
||||
"some_other_script.py",
|
||||
"a_module",
|
||||
"pip install a_module && ./some_other_script.py",
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"script, filename, module_name, corrected_script", positive_tests
|
||||
)
|
||||
def test_match(script, filename, module_name, corrected_script, module_error_output):
|
||||
assert match(Command(script, module_error_output))
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"script, filename, module_name, corrected_script", positive_tests
|
||||
)
|
||||
def test_get_new_command(
|
||||
script, filename, module_name, corrected_script, module_error_output
|
||||
):
|
||||
assert get_new_command(Command(script, module_error_output)) == corrected_script
|
@ -43,7 +43,7 @@ class TestSettingsFromFile(object):
|
||||
assert settings.rules == const.DEFAULT_RULES + ['test']
|
||||
|
||||
|
||||
@pytest.mark.usefixture('load_source')
|
||||
@pytest.mark.usefixtures('load_source')
|
||||
class TestSettingsFromEnv(object):
|
||||
def test_from_env(self, os_environ, settings):
|
||||
os_environ.update({'THEFUCK_RULES': 'bash:lisp',
|
||||
|
@ -8,14 +8,15 @@ from thefuck.types import Command
|
||||
from thefuck.corrector import get_corrected_commands, organize_commands
|
||||
|
||||
|
||||
class TestGetRules(object):
|
||||
@pytest.fixture
|
||||
def glob(self, mocker):
|
||||
results = {}
|
||||
mocker.patch('thefuck.system.Path.glob',
|
||||
new_callable=lambda: lambda *_: results.pop('value', []))
|
||||
return lambda value: results.update({'value': value})
|
||||
@pytest.fixture
|
||||
def glob(mocker):
|
||||
results = {}
|
||||
mocker.patch('thefuck.system.Path.glob',
|
||||
new_callable=lambda: lambda *_: results.pop('value', []))
|
||||
return lambda value: results.update({'value': value})
|
||||
|
||||
|
||||
class TestGetRules(object):
|
||||
@pytest.fixture(autouse=True)
|
||||
def load_source(self, monkeypatch):
|
||||
monkeypatch.setattr('thefuck.types.load_source',
|
||||
@ -39,6 +40,14 @@ class TestGetRules(object):
|
||||
self._compare_names(rules, loaded_rules)
|
||||
|
||||
|
||||
def test_get_rules_rule_exception(mocker, glob):
|
||||
load_source = mocker.patch('thefuck.types.load_source',
|
||||
side_effect=ImportError("No module named foo..."))
|
||||
glob([Path('git.py')])
|
||||
assert not corrector.get_rules()
|
||||
load_source.assert_called_once_with('git', 'git.py')
|
||||
|
||||
|
||||
def test_get_corrected_commands(mocker):
|
||||
command = Command('test', 'test')
|
||||
rules = [Rule(match=lambda _: False),
|
||||
|
@ -45,6 +45,12 @@ class TestCorrectedCommand(object):
|
||||
|
||||
|
||||
class TestRule(object):
|
||||
def test_from_path_rule_exception(self, mocker):
|
||||
load_source = mocker.patch('thefuck.types.load_source',
|
||||
side_effect=ImportError("No module named foo..."))
|
||||
assert Rule.from_path(Path('git.py')) is None
|
||||
load_source.assert_called_once_with('git', 'git.py')
|
||||
|
||||
def test_from_path(self, mocker):
|
||||
match = object()
|
||||
get_new_command = object()
|
||||
@ -60,20 +66,22 @@ class TestRule(object):
|
||||
== Rule('bash', match, get_new_command, priority=900))
|
||||
load_source.assert_called_once_with('bash', rule_path)
|
||||
|
||||
@pytest.mark.parametrize('rules, exclude_rules, rule, is_enabled', [
|
||||
(const.DEFAULT_RULES, [], Rule('git', enabled_by_default=True), True),
|
||||
(const.DEFAULT_RULES, [], Rule('git', enabled_by_default=False), False),
|
||||
([], [], Rule('git', enabled_by_default=False), False),
|
||||
([], [], Rule('git', enabled_by_default=True), False),
|
||||
(const.DEFAULT_RULES + ['git'], [], Rule('git', enabled_by_default=False), True),
|
||||
(['git'], [], Rule('git', enabled_by_default=False), True),
|
||||
(const.DEFAULT_RULES, ['git'], Rule('git', enabled_by_default=True), False),
|
||||
(const.DEFAULT_RULES, ['git'], Rule('git', enabled_by_default=False), False),
|
||||
([], ['git'], Rule('git', enabled_by_default=True), False),
|
||||
([], ['git'], Rule('git', enabled_by_default=False), False)])
|
||||
def test_is_enabled(self, settings, rules, exclude_rules, rule, is_enabled):
|
||||
settings.update(rules=rules,
|
||||
exclude_rules=exclude_rules)
|
||||
def test_from_path_excluded_rule(self, mocker, settings):
|
||||
load_source = mocker.patch('thefuck.types.load_source')
|
||||
settings.update(exclude_rules=['git'])
|
||||
rule_path = os.path.join(os.sep, 'rules', 'git.py')
|
||||
assert Rule.from_path(Path(rule_path)) is None
|
||||
assert not load_source.called
|
||||
|
||||
@pytest.mark.parametrize('rules, rule, is_enabled', [
|
||||
(const.DEFAULT_RULES, Rule('git', enabled_by_default=True), True),
|
||||
(const.DEFAULT_RULES, Rule('git', enabled_by_default=False), False),
|
||||
([], Rule('git', enabled_by_default=False), False),
|
||||
([], Rule('git', enabled_by_default=True), False),
|
||||
(const.DEFAULT_RULES + ['git'], Rule('git', enabled_by_default=False), True),
|
||||
(['git'], Rule('git', enabled_by_default=False), True)])
|
||||
def test_is_enabled(self, settings, rules, rule, is_enabled):
|
||||
settings.update(rules=rules)
|
||||
assert rule.is_enabled == is_enabled
|
||||
|
||||
def test_isnt_match(self):
|
||||
@ -131,6 +139,7 @@ class TestCommand(object):
|
||||
env=os_environ)
|
||||
|
||||
@pytest.mark.parametrize('script, result', [
|
||||
([], None),
|
||||
([''], None),
|
||||
(['', ''], None),
|
||||
(['ls', '-la'], 'ls -la'),
|
||||
|
@ -15,7 +15,7 @@ def get_loaded_rules(rules_paths):
|
||||
for path in rules_paths:
|
||||
if path.name != '__init__.py':
|
||||
rule = Rule.from_path(path)
|
||||
if rule.is_enabled:
|
||||
if rule and rule.is_enabled:
|
||||
yield rule
|
||||
|
||||
|
||||
|
@ -23,6 +23,7 @@ def _get_raw_command(known_args):
|
||||
diff = SequenceMatcher(a=alias, b=command).ratio()
|
||||
if diff < const.DIFF_WITH_ALIAS or command in executables:
|
||||
return [command]
|
||||
return []
|
||||
|
||||
|
||||
def fix_command(known_args):
|
||||
|
13
thefuck/rules/python_module_error.py
Normal file
13
thefuck/rules/python_module_error.py
Normal file
@ -0,0 +1,13 @@
|
||||
import re
|
||||
from thefuck.shells import shell
|
||||
|
||||
MISSING_MODULE = r"ModuleNotFoundError: No module named '([^']+)'"
|
||||
|
||||
|
||||
def match(command):
|
||||
return "ModuleNotFoundError: No module named '" in command.output
|
||||
|
||||
|
||||
def get_new_command(command):
|
||||
missing_module = re.findall(MISSING_MODULE, command.output)[0]
|
||||
return shell.and_("pip install {}".format(missing_module), command.script)
|
@ -136,9 +136,16 @@ class Rule(object):
|
||||
|
||||
"""
|
||||
name = path.name[:-3]
|
||||
if name in settings.exclude_rules:
|
||||
logs.debug(u'Ignoring excluded rule: {}'.format(name))
|
||||
return
|
||||
with logs.debug_time(u'Importing rule: {};'.format(name)):
|
||||
rule_module = load_source(name, str(path))
|
||||
priority = getattr(rule_module, 'priority', DEFAULT_PRIORITY)
|
||||
try:
|
||||
rule_module = load_source(name, str(path))
|
||||
except Exception:
|
||||
logs.exception(u"Rule {} failed to load".format(name), sys.exc_info())
|
||||
return
|
||||
priority = getattr(rule_module, 'priority', DEFAULT_PRIORITY)
|
||||
return cls(name, rule_module.match,
|
||||
rule_module.get_new_command,
|
||||
getattr(rule_module, 'enabled_by_default', True),
|
||||
@ -153,14 +160,11 @@ class Rule(object):
|
||||
:rtype: bool
|
||||
|
||||
"""
|
||||
if self.name in settings.exclude_rules:
|
||||
return False
|
||||
elif self.name in settings.rules:
|
||||
return True
|
||||
elif self.enabled_by_default and ALL_ENABLED in settings.rules:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
return (
|
||||
self.name in settings.rules
|
||||
or self.enabled_by_default
|
||||
and ALL_ENABLED in settings.rules
|
||||
)
|
||||
|
||||
def is_match(self, command):
|
||||
"""Returns `True` if rule matches the command.
|
||||
|
Loading…
x
Reference in New Issue
Block a user