1
0
mirror of https://github.com/nvbn/thefuck.git synced 2025-01-18 12:06:04 +00:00

Initial commit

This commit is contained in:
nvbn 2015-04-08 18:15:49 +02:00
commit 71f1f4224b
11 changed files with 362 additions and 0 deletions

60
.gitignore vendored Normal file
View File

@ -0,0 +1,60 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
# C extensions
*.so
# Distribution / packaging
.Python
env/
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
*.egg-info/
.installed.cfg
*.egg
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*,cover
# Translations
*.mo
*.pot
# Django stuff:
*.log
# Sphinx documentation
docs/_build/
# PyBuilder
target/
.env
.idea

56
README.md Normal file
View File

@ -0,0 +1,56 @@
# The Fuck
Magnificent app which corrects your previous console command.
Few examples:
```bash
➜ apt-get install vim
E: Could not open lock file /var/lib/dpkg/lock - open (13: Permission denied)
E: Unable to lock the administration directory (/var/lib/dpkg/), are you root?
➜ fuck
[sudo] password for nvbn:
Reading package lists... Done
...
➜ git push
fatal: The current branch master has no upstream branch.
To push the current branch and set the remote as upstream, use
git push --set-upstream origin master
➜ fuck
Counting objects: 9, done.
...
```
## Installation
Install `The Fuck`:
```bash
sudo pip3 install thefuck
```
And add to `.bashrc` or `.zshrc`:
```bash
alias fuck='$(thefuck $(fc -ln -1))'
```
## Developing
Install `The Fuck` for development:
```bash
pip3 install -r requirements.txt
python3 setup.py develop
```
Run tests:
```bash
py.test
```

1
requirements.txt Normal file
View File

@ -0,0 +1 @@
pytest

3
setup.cfg Normal file
View File

@ -0,0 +1,3 @@
[egg_info]
tag_build = dev
tag_svn_revision = true

26
setup.py Normal file
View File

@ -0,0 +1,26 @@
from setuptools import setup, find_packages
version = '1'
setup(name='thefuck',
version=version,
description="",
long_description="""\
""",
classifiers=[], # Get strings from http://pypi.python.org/pypi?%3Aaction=list_classifiers
keywords='',
author='',
author_email='',
url='',
license='',
packages=find_packages(exclude=['ez_setup', 'examples', 'tests']),
include_package_data=True,
zip_safe=False,
install_requires=[
# -*- Extra requirements: -*-
],
entry_points={'console_scripts': [
'thefuck = thefuck.main:main',
]},
)

View File

@ -0,0 +1,24 @@
import pytest
from thefuck.main import Command
from thefuck.rules.git_push import match, get_new_command
@pytest.fixture
def stderr():
return '''fatal: The current branch master has no upstream branch.
To push the current branch and set the remote as upstream, use
git push --set-upstream origin master
'''
def test_match(stderr):
assert match(Command('git push master', '', stderr))
assert not match(Command('git push master', '', ''))
assert not match(Command('ls', '', stderr))
def test_get_new_command(stderr):
assert get_new_command(Command('', '', stderr))\
== "git push --set-upstream origin master"

12
tests/rules/test_sudo.py Normal file
View File

@ -0,0 +1,12 @@
from thefuck.main import Command
from thefuck.rules.sudo import match, get_new_command
def test_match():
assert match(Command('', '', 'Permission denied'))
assert match(Command('', '', 'permission denied'))
assert not match(Command('', '', ''))
def test_get_new_command():
assert get_new_command(Command('ls', '', '')) == 'sudo ls'

81
tests/test_main.py Normal file
View File

@ -0,0 +1,81 @@
from unittest.mock import patch, Mock
from subprocess import PIPE
from pathlib import PosixPath, Path
from thefuck import main
def test_setup_user_dir():
with patch('thefuck.main.Path.is_dir', return_value=False), \
patch('thefuck.main.Path.mkdir') as mkdir, \
patch('thefuck.main.Path.touch') as touch:
main.setup_user_dir()
assert mkdir.call_count == 2
assert touch.call_count == 1
with patch('thefuck.main.Path.is_dir', return_value=True), \
patch('thefuck.main.Path.mkdir') as mkdir, \
patch('thefuck.main.Path.touch') as touch:
main.setup_user_dir()
assert mkdir.call_count == 0
assert touch.call_count == 0
def test_get_settings():
with patch('thefuck.main.load_source', return_value=Mock(rules=['bash'])):
assert main.get_settings(Path('/')).rules == ['bash']
with patch('thefuck.main.load_source', return_value=Mock(spec=[])):
assert main.get_settings(Path('/')).rules is None
def test_is_rule_enabled():
assert main.is_rule_enabled(main.Settings(None), Path('bash.py'))
assert main.is_rule_enabled(main.Settings(['bash']), Path('bash.py'))
assert not main.is_rule_enabled(main.Settings(['bash']), Path('lisp.py'))
def test_load_rule():
match = object()
get_new_command = object()
with patch('thefuck.main.load_source',
return_value=Mock(
match=match,
get_new_command=get_new_command)) as load_source:
assert main.load_rule(Path('/rules/bash.py')) == main.Rule(match, get_new_command)
load_source.assert_called_once_with('bash', '/rules/bash.py')
def test_get_rules():
with patch('thefuck.main.Path.glob') as glob, \
patch('thefuck.main.load_source',
lambda x, _: Mock(match=x, get_new_command=x)):
glob.return_value = [PosixPath('bash.py'), PosixPath('lisp.py')]
assert main.get_rules(
Path('~'),
main.Settings(None)) == [main.Rule('bash', 'bash'),
main.Rule('lisp', 'lisp'),
main.Rule('bash', 'bash'),
main.Rule('lisp', 'lisp')]
assert main.get_rules(
Path('~'),
main.Settings(['bash'])) == [main.Rule('bash', 'bash'),
main.Rule('bash', 'bash')]
def test_get_command():
with patch('thefuck.main.Popen') as Popen:
Popen.return_value.stdout.read.return_value = b'stdout'
Popen.return_value.stderr.read.return_value = b'stderr'
assert main.get_command(['thefuck', 'apt-get', 'search', 'vim']) \
== main.Command('apt-get search vim', 'stdout', 'stderr')
Popen.assert_called_once_with('apt-get search vim',
shell=True,
stdout=PIPE,
stderr=PIPE)
def test_get_matched_rule():
rules = [main.Rule(lambda x: x.script == 'cd ..', None),
main.Rule(lambda _: False, None)]
assert main.get_matched_rule(main.Command('ls', '', ''),
rules) is None
assert main.get_matched_rule(main.Command('cd ..', '', ''),
rules) == rules[0]

85
thefuck/main.py Normal file
View File

@ -0,0 +1,85 @@
from collections import namedtuple
from imp import load_source
from pathlib import Path
from os.path import expanduser
from subprocess import Popen, PIPE
import sys
Command = namedtuple('Command', ('script', 'stdout', 'stderr'))
Settings = namedtuple('Settings', ('rules',))
Rule = namedtuple('Rule', ('match', 'get_new_command'))
def setup_user_dir() -> Path:
"""Returns user config dir, create it when it doesn't exists."""
user_dir = Path(expanduser('~/.thefuck'))
if not user_dir.is_dir():
user_dir.mkdir()
user_dir.joinpath('rules').mkdir()
user_dir.joinpath('settings.py').touch()
return user_dir
def get_settings(user_dir: Path) -> Settings:
"""Returns prepared settings module."""
settings = load_source('settings',
str(user_dir.joinpath('settings.py')))
return Settings(getattr(settings, 'rules', None))
def is_rule_enabled(settings: Settings, rule: Path) -> bool:
"""Returns `True` when rule mentioned in `rules` or `rules`
isn't defined.
"""
return settings.rules is None or rule.name[:-3] in settings.rules
def load_rule(rule: Path) -> Rule:
"""Imports rule module and returns it."""
rule_module = load_source(rule.name[:-3], str(rule))
return Rule(rule_module.match, rule_module.get_new_command)
def get_rules(user_dir: Path, settings: Settings) -> [Rule]:
"""Returns all enabled rules."""
bundled = Path(__file__).parent\
.joinpath('rules')\
.glob('*.py')
user = user_dir.joinpath('rules').glob('*.py')
return [load_rule(rule) for rule in list(bundled) + list(user)
if is_rule_enabled(settings, rule)]
def get_command(args: [str]) -> Command:
"""Creates command from `args` and executes it."""
script = ' '.join(args[1:])
result = Popen(script, shell=True, stdout=PIPE, stderr=PIPE)
return Command(script, result.stdout.read().decode(),
result.stderr.read().decode())
def get_matched_rule(command: Command, rules: [Rule]) -> Rule:
"""Returns first matched rule for command."""
for rule in rules:
if rule.match(command):
return rule
def run_rule(rule: Rule, command: Command):
"""Runs command from rule for passed command."""
new_command = rule.get_new_command(command)
print(new_command)
def main():
command = get_command(sys.argv)
user_dir = setup_user_dir()
settings = get_settings(user_dir)
rules = get_rules(user_dir, settings)
matched_rule = get_matched_rule(command, rules)
if matched_rule:
run_rule(matched_rule, command)
else:
print('echo No fuck given')

View File

@ -0,0 +1,8 @@
def match(command):
return ('git' in command.script
and 'push' in command.script
and 'set-upstream' in command.stderr)
def get_new_command(command):
return command.stderr.split('\n')[-3].strip()

6
thefuck/rules/sudo.py Normal file
View File

@ -0,0 +1,6 @@
def match(command):
return 'permission denied' in command.stderr.lower()
def get_new_command(command):
return 'sudo {}'.format(command.script)