1
0
mirror of https://github.com/nvbn/thefuck.git synced 2025-11-01 15:42:06 +00:00

Compare commits

...

25 Commits
1.35 ... 1.38

Author SHA1 Message Date
nvbn
f082ba829f Bump to 1.38 2015-05-08 15:27:33 +02:00
Vladimir Iakovlev
112e20d7c5 Merge pull request #171 from mcarton/dry
Add a don't repeat yourself rule
2015-05-08 12:11:41 +02:00
mcarton
95007220fb Add a test for the DRY rule 2015-05-08 11:42:00 +02:00
mcarton
56f636f3d8 Remove unnecessary space in the DRY rule 2015-05-08 11:41:26 +02:00
mcarton
932a7c5db5 Add a don't repeat yourself rule 2015-05-08 01:49:47 +02:00
Vladimir Iakovlev
1bed4d4e8d Merge pull request #170 from SanketDG/manfix
add rule for having no spaces in man commands.
2015-05-08 01:11:48 +02:00
Vladimir Iakovlev
e0bba379ff Merge pull request #169 from mcarton/git-checkout
Add the git_checkout rule
2015-05-08 01:11:09 +02:00
SanketDG
045959ec47 add man_no_space 2015-05-08 00:16:50 +05:30
SanketDG
65aeea857e add tests for man_no_space 2015-05-08 00:15:57 +05:30
SanketDG
793e883073 add man_no_space command 2015-05-08 00:15:32 +05:30
mcarton
a395ac568c Add the git_checkout rule
It creates a branch before checking-out to it if the branch does not
exist.
2015-05-07 20:32:04 +02:00
nvbn
29e70e14a0 Bump to 1.37 2015-05-07 14:16:17 +02:00
nvbn
0cdd23edcf Use wheel 2015-05-07 14:16:07 +02:00
nvbn
36d80859a4 Add tox config 2015-05-07 13:51:27 +02:00
nvbn
2b12b4bfce Improve tests with mocker 2015-05-07 13:42:52 +02:00
nvbn
91c1fe414a Update thefuck-alias entry point 2015-05-07 13:32:23 +02:00
nvbn
f3d377114e Bump to 1.36 2015-05-07 13:12:25 +02:00
nvbn
05f594b918 #154 Add ability to override priority in settings 2015-05-07 13:11:45 +02:00
nvbn
5bf1424613 #164 Decrease priority of no_command 2015-05-07 12:57:43 +02:00
nvbn
fc3fcf028a #154 Add priority to rules 2015-05-06 13:57:09 +02:00
nvbn
5864faadef #165 fix python 2 support 2015-05-06 13:17:14 +02:00
Vladimir Iakovlev
608d48e408 Merge pull request #166 from mcarton/git-add
Add a git_add rule
2015-05-06 13:10:23 +02:00
mcarton
9380eb1f56 Add a git_add rule 2015-05-06 11:31:31 +02:00
Vladimir Iakovlev
fb069b74d7 Merge pull request #159 from mcarton/master
Add a rule for pacman
2015-05-05 13:30:03 +02:00
mcarton
6624ecb3b8 Add a rule for pacman 2015-05-05 11:13:29 +02:00
23 changed files with 273 additions and 86 deletions

View File

@@ -144,13 +144,18 @@ using matched rule and run it. Rules enabled by default:
* `cd_parent` – changes `cd..` to `cd ..`;
* `cd_mkdir` – creates directories before cd'ing into them;
* `cp_omitting_directory` – adds `-a` when you `cp` directory;
* `dry` – fix repetitions like "git git push";
* `fix_alt_space` – replaces Alt+Space with Space character;
* `git_add` – fix *"Did you forget to 'git add'?"*;
* `git_checkout` – creates the branch before checking-out;
* `git_no_command` – fixes wrong git commands like `git brnch`;
* `git_push` – adds `--set-upstream origin $branch` to previous failed `git push`;
* `has_exists_script` – prepends `./` when script/binary exists;
* `lein_not_task` – fixes wrong `lein` tasks like `lein rpl`;
* `mkdir_p` – adds `-p` when you trying to create directory without parent;
* `no_command` – fixes wrong console commands, for example `vom/vim`;
* `man_no_space` – fixes man commands without spaces, for example `mandiff`;
* `pacman` – installs app with `pacman` or `yaourt` if it is not installed;
* `pip_unknown_command` – fixes wrong pip commands, for example `pip instatl/pip install`;
* `python_command` – prepends `python` when you trying to run not executable/without `./` python script;
* `sl_ls` – changes `sl` to `ls`;
@@ -191,12 +196,14 @@ def match(command, settings):
def get_new_command(command, settings):
return 'sudo {}'.format(command.script)
# Optional:
enabled_by_default = True
def side_effect(command, settings):
subprocess.call('chmod 777 .', shell=True)
priority = 1000 # Lower first
```
[More examples of rules](https://github.com/nvbn/thefuck/tree/master/thefuck/rules),
@@ -209,7 +216,8 @@ The Fuck has a few settings parameters, they can be changed in `~/.thefuck/setti
* `rules` – list of enabled rules, by default `thefuck.conf.DEFAULT_RULES`;
* `require_confirmation` – require confirmation before running new command, by default `False`;
* `wait_command` – max amount of time in seconds for getting previous command output;
* `no_colors` – disable colored output.
* `no_colors` – disable colored output;
* `priority` – dict with rules priorities, rule with lower `priority` will be matched first.
Example of `settings.py`:
@@ -218,6 +226,7 @@ rules = ['sudo', 'no_command']
require_confirmation = True
wait_command = 10
no_colors = False
priority = {'sudo': 100, 'no_command': 9999}
```
Or via environment variables:
@@ -225,7 +234,9 @@ Or via environment variables:
* `THEFUCK_RULES` – list of enabled rules, like `DEFAULT_RULES:rm_root` or `sudo:no_command`;
* `THEFUCK_REQUIRE_CONFIRMATION` – require confirmation before running new command, `true/false`;
* `THEFUCK_WAIT_COMMAND` – max amount of time in seconds for getting previous command output;
* `THEFUCK_NO_COLORS` – disable colored output, `true/false`.
* `THEFUCK_NO_COLORS` – disable colored output, `true/false`;
* `THEFUCK_PRIORITY` – priority of the rules, like `no_command=9999:apt_get=100`,
rule with lower `priority` will be matched first.
For example:
@@ -234,6 +245,7 @@ export THEFUCK_RULES='sudo:no_command'
export THEFUCK_REQUIRE_CONFIRMATION='true'
export THEFUCK_WAIT_COMMAND=10
export THEFUCK_NO_COLORS='false'
export THEFUCK_PRIORITY='no_command=9999:apt_get=100'
```
## Developing

View File

@@ -28,4 +28,4 @@ call('git commit -am "Bump to {}"'.format(version), shell=True)
call('git tag {}'.format(version), shell=True)
call('git push', shell=True)
call('git push --tags', shell=True)
call('python setup.py sdist upload', shell=True)
call('python setup.py sdist bdist_wheel upload', shell=True)

View File

@@ -1,2 +1,4 @@
pytest
mock
pytest-mock
wheel

2
setup.cfg Normal file
View File

@@ -0,0 +1,2 @@
[bdist_wheel]
universal = 1

View File

@@ -1,7 +1,7 @@
from setuptools import setup, find_packages
VERSION = '1.35'
VERSION = '1.38'
setup(name='thefuck',
@@ -17,4 +17,5 @@ setup(name='thefuck',
zip_safe=False,
install_requires=['pathlib', 'psutil', 'colorama', 'six'],
entry_points={'console_scripts': [
'thefuck = thefuck.main:main', 'thefuck-alias = thefuck.main:alias']})
'thefuck = thefuck.main:main',
'thefuck-alias = thefuck.shells:app_alias']})

17
tests/rules/test_dry.py Normal file
View File

@@ -0,0 +1,17 @@
import pytest
from thefuck.rules.dry import match, get_new_command
from tests.utils import Command
@pytest.mark.parametrize('command', [
Command(script='cd cd foo'),
Command(script='git git push origin/master')])
def test_match(command):
assert match(command, None)
@pytest.mark.parametrize('command, new_command', [
(Command('cd cd foo'), 'cd foo'),
(Command('git git push origin/master'), 'git push origin/master')])
def test_get_new_command(command, new_command):
assert get_new_command(command, None) == new_command

View File

@@ -0,0 +1,12 @@
from thefuck.rules.man_no_space import match, get_new_command
from tests.utils import Command
def test_match():
assert match(Command('mandiff', stderr='mandiff: command not found'), None)
assert not match(Command(), None)
def test_get_new_command():
assert get_new_command(
Command('mandiff'), None) == 'man diff'

View File

@@ -14,10 +14,8 @@ def test_default(enabled, rules, result):
@pytest.fixture
def load_source(monkeypatch):
mock = Mock()
monkeypatch.setattr('thefuck.conf.load_source', mock)
return mock
def load_source(mocker):
return mocker.patch('thefuck.conf.load_source')
@pytest.fixture
@@ -40,12 +38,14 @@ class TestSettingsFromFile(object):
load_source.return_value = Mock(rules=['test'],
wait_command=10,
require_confirmation=True,
no_colors=True)
no_colors=True,
priority={'vim': 100})
settings = conf.get_settings(Mock())
assert settings.rules == ['test']
assert settings.wait_command == 10
assert settings.require_confirmation is True
assert settings.no_colors is True
assert settings.priority == {'vim': 100}
def test_from_file_with_DEFAULT(self, load_source):
load_source.return_value = Mock(rules=conf.DEFAULT_RULES + ['test'],
@@ -62,12 +62,14 @@ class TestSettingsFromEnv(object):
environ.update({'THEFUCK_RULES': 'bash:lisp',
'THEFUCK_WAIT_COMMAND': '55',
'THEFUCK_REQUIRE_CONFIRMATION': 'true',
'THEFUCK_NO_COLORS': 'false'})
'THEFUCK_NO_COLORS': 'false',
'THEFUCK_PRIORITY': 'bash=10:lisp=wrong:vim=15'})
settings = conf.get_settings(Mock())
assert settings.rules == ['bash', 'lisp']
assert settings.wait_command == 55
assert settings.require_confirmation is True
assert settings.no_colors is False
assert settings.priority == {'bash': 10, 'vim': 15}
def test_from_env_with_DEFAULT(self, environ):
environ.update({'THEFUCK_RULES': 'DEFAULT_RULES:bash:lisp'})

View File

@@ -6,34 +6,55 @@ from thefuck import main, conf, types
from tests.utils import Rule, Command
def test_load_rule(monkeypatch):
def test_load_rule(mocker):
match = object()
get_new_command = object()
load_source = Mock()
load_source.return_value = Mock(match=match,
get_new_command=get_new_command,
enabled_by_default=True)
monkeypatch.setattr('thefuck.main.load_source', load_source)
load_source = mocker.patch(
'thefuck.main.load_source',
return_value=Mock(match=match,
get_new_command=get_new_command,
enabled_by_default=True,
priority=900))
assert main.load_rule(Path('/rules/bash.py')) \
== Rule('bash', match, get_new_command)
== Rule('bash', match, get_new_command, priority=900)
load_source.assert_called_once_with('bash', '/rules/bash.py')
@pytest.mark.parametrize('conf_rules, rules', [
(conf.DEFAULT_RULES, [Rule('bash', 'bash', 'bash'),
Rule('lisp', 'lisp', 'lisp'),
Rule('bash', 'bash', 'bash'),
Rule('lisp', 'lisp', 'lisp')]),
(types.RulesNamesList(['bash']), [Rule('bash', 'bash', 'bash'),
Rule('bash', 'bash', 'bash')])])
def test_get_rules(monkeypatch, conf_rules, rules):
monkeypatch.setattr(
'thefuck.main.Path.glob',
lambda *_: [PosixPath('bash.py'), PosixPath('lisp.py')])
monkeypatch.setattr('thefuck.main.load_source',
lambda x, _: Mock(match=x, get_new_command=x,
enabled_by_default=True))
assert list(main.get_rules(Path('~'), Mock(rules=conf_rules))) == rules
class TestGetRules(object):
@pytest.fixture(autouse=True)
def glob(self, mocker):
return mocker.patch('thefuck.main.Path.glob', return_value=[])
def _compare_names(self, rules, names):
return [r.name for r in rules] == names
@pytest.mark.parametrize('conf_rules, rules', [
(conf.DEFAULT_RULES, ['bash', 'lisp', 'bash', 'lisp']),
(types.RulesNamesList(['bash']), ['bash', 'bash'])])
def test_get(self, monkeypatch, glob, conf_rules, rules):
glob.return_value = [PosixPath('bash.py'), PosixPath('lisp.py')]
monkeypatch.setattr('thefuck.main.load_source',
lambda x, _: Rule(x))
assert self._compare_names(
main.get_rules(Path('~'), Mock(rules=conf_rules, priority={})),
rules)
@pytest.mark.parametrize('priority, unordered, ordered', [
({},
[Rule('bash', priority=100), Rule('python', priority=5)],
['python', 'bash']),
({},
[Rule('lisp', priority=9999), Rule('c', priority=conf.DEFAULT_PRIORITY)],
['c', 'lisp']),
({'python': 9999},
[Rule('bash', priority=100), Rule('python', priority=5)],
['bash', 'python'])])
def test_ordered_by_priority(self, monkeypatch, priority, unordered, ordered):
monkeypatch.setattr('thefuck.main._get_loaded_rules',
lambda *_: unordered)
assert self._compare_names(
main.get_rules(Path('~'), Mock(priority=priority)),
ordered)
class TestGetCommand(object):
@@ -64,6 +85,7 @@ class TestGetCommand(object):
stdout=PIPE,
stderr=PIPE,
env={'LANG': 'C'})
@pytest.mark.parametrize('args, result', [
(['thefuck', 'ls', '-la'], 'ls -la'),
(['thefuck', 'ls'], 'ls')])
@@ -94,10 +116,8 @@ class TestGetMatchedRule(object):
class TestRunRule(object):
@pytest.fixture(autouse=True)
def confirm(self, monkeypatch):
mock = Mock(return_value=True)
monkeypatch.setattr('thefuck.main.confirm', mock)
return mock
def confirm(self, mocker):
return mocker.patch('thefuck.main.confirm', return_value=True)
def test_run_rule(self, capsys):
main.run_rule(Rule(get_new_command=lambda *_: 'new-command'),
@@ -123,10 +143,8 @@ class TestRunRule(object):
class TestConfirm(object):
@pytest.fixture
def stdin(self, monkeypatch):
mock = Mock(return_value='\n')
monkeypatch.setattr('sys.stdin.read', mock)
return mock
def stdin(self, mocker):
return mocker.patch('sys.stdin.read', return_value='\n')
def test_when_not_required(self, capsys):
assert main.confirm('command', None, Mock(require_confirmation=False))

View File

@@ -1,20 +1,15 @@
import pytest
from mock import Mock, MagicMock
from thefuck import shells
@pytest.fixture
def builtins_open(monkeypatch):
mock = MagicMock()
monkeypatch.setattr('six.moves.builtins.open', mock)
return mock
def builtins_open(mocker):
return mocker.patch('six.moves.builtins.open')
@pytest.fixture
def isfile(monkeypatch):
mock = Mock(return_value=True)
monkeypatch.setattr('os.path.isfile', mock)
return mock
def isfile(mocker):
return mocker.patch('os.path.isfile', return_value=True)
class TestGeneric(object):
@@ -32,9 +27,8 @@ class TestGeneric(object):
@pytest.mark.usefixtures('isfile')
class TestBash(object):
@pytest.fixture(autouse=True)
def Popen(self, monkeypatch):
mock = Mock()
monkeypatch.setattr('thefuck.shells.Popen', mock)
def Popen(self, mocker):
mock = mocker.patch('thefuck.shells.Popen')
mock.return_value.stdout.read.return_value = (
b'alias l=\'ls -CF\'\n'
b'alias la=\'ls -A\'\n'
@@ -52,16 +46,15 @@ class TestBash(object):
def test_put_to_history(self, builtins_open):
shells.Bash().put_to_history('ls')
builtins_open.return_value.__enter__.return_value.\
builtins_open.return_value.__enter__.return_value. \
write.assert_called_once_with('ls\n')
@pytest.mark.usefixtures('isfile')
class TestZsh(object):
@pytest.fixture(autouse=True)
def Popen(self, monkeypatch):
mock = Mock()
monkeypatch.setattr('thefuck.shells.Popen', mock)
def Popen(self, mocker):
mock = mocker.patch('thefuck.shells.Popen')
mock.return_value.stdout.read.return_value = (
b'l=\'ls -CF\'\n'
b'la=\'ls -A\'\n'
@@ -77,9 +70,9 @@ class TestZsh(object):
def test_to_shell(self):
assert shells.Zsh().to_shell('pwd') == 'pwd'
def test_put_to_history(self, builtins_open, monkeypatch):
monkeypatch.setattr('thefuck.shells.time',
lambda: 1430707243.3517463)
def test_put_to_history(self, builtins_open, mocker):
mocker.patch('thefuck.shells.time',
return_value=1430707243.3517463)
shells.Zsh().put_to_history('ls')
builtins_open.return_value.__enter__.return_value. \
write.assert_called_once_with(': 1430707243:0;ls\n')
write.assert_called_once_with(': 1430707243:0;ls\n')

View File

@@ -1,4 +1,5 @@
from thefuck import types
from thefuck.conf import DEFAULT_PRIORITY
def Command(script='', stdout='', stderr=''):
@@ -8,6 +9,8 @@ def Command(script='', stdout='', stderr=''):
def Rule(name='', match=lambda *_: True,
get_new_command=lambda *_: '',
enabled_by_default=True,
side_effect=None):
side_effect=None,
priority=DEFAULT_PRIORITY):
return types.Rule(name, match, get_new_command,
enabled_by_default, side_effect)
enabled_by_default, side_effect,
priority)

View File

@@ -22,17 +22,20 @@ class _DefaultRulesNames(types.RulesNamesList):
DEFAULT_RULES = _DefaultRulesNames([])
DEFAULT_PRIORITY = 1000
DEFAULT_SETTINGS = {'rules': DEFAULT_RULES,
'wait_command': 3,
'require_confirmation': False,
'no_colors': False}
'no_colors': False,
'priority': {}}
ENV_TO_ATTR = {'THEFUCK_RULES': 'rules',
'THEFUCK_WAIT_COMMAND': 'wait_command',
'THEFUCK_REQUIRE_CONFIRMATION': 'require_confirmation',
'THEFUCK_NO_COLORS': 'no_colors'}
'THEFUCK_NO_COLORS': 'no_colors',
'THEFUCK_PRIORITY': 'priority'}
SETTINGS_HEADER = u"""# ~/.thefuck/settings.py: The Fuck settings file
@@ -65,16 +68,29 @@ def _rules_from_env(val):
return val
def _priority_from_env(val):
"""Gets priority pairs from env."""
for part in val.split(':'):
try:
rule, priority = part.split('=')
yield rule, int(priority)
except ValueError:
continue
def _val_from_env(env, attr):
"""Transforms env-strings to python."""
val = os.environ[env]
if attr == 'rules':
val = _rules_from_env(val)
return _rules_from_env(val)
elif attr == 'priority':
return dict(_priority_from_env(val))
elif attr == 'wait_command':
val = int(val)
return int(val)
elif attr in ('require_confirmation', 'no_colors'):
val = val.lower() == 'true'
return val
return val.lower() == 'true'
else:
return val
def _settings_from_env():

View File

@@ -26,7 +26,17 @@ def load_rule(rule):
return types.Rule(rule.name[:-3], rule_module.match,
rule_module.get_new_command,
getattr(rule_module, 'enabled_by_default', True),
getattr(rule_module, 'side_effect', None))
getattr(rule_module, 'side_effect', None),
getattr(rule_module, 'priority', conf.DEFAULT_PRIORITY))
def _get_loaded_rules(rules, settings):
"""Yields all available rules."""
for rule in rules:
if rule.name != '__init__.py':
loaded_rule = load_rule(rule)
if loaded_rule in settings.rules:
yield loaded_rule
def get_rules(user_dir, settings):
@@ -35,11 +45,9 @@ def get_rules(user_dir, settings):
.joinpath('rules') \
.glob('*.py')
user = user_dir.joinpath('rules').glob('*.py')
for rule in sorted(list(bundled)) + list(user):
if rule.name != '__init__.py':
loaded_rule = load_rule(rule)
if loaded_rule in settings.rules:
yield loaded_rule
rules = _get_loaded_rules(sorted(bundled) + sorted(user), settings)
return sorted(rules, key=lambda rule: settings.priority.get(
rule.name, rule.priority))
def wait_output(settings, popen):
@@ -113,10 +121,6 @@ def run_rule(rule, command, settings):
print(new_command)
def alias():
print(shells.app_alias())
def main():
colorama.init()
user_dir = setup_user_dir()

12
thefuck/rules/dry.py Normal file
View File

@@ -0,0 +1,12 @@
def match(command, settings):
split_command = command.script.split()
return len(split_command) >= 2 and split_command[0] == split_command[1]
def get_new_command(command, settings):
return command.script[command.script.find(' ')+1:]
# it should be rare enough to actually have to type twice the same word, so
# this rule can have a higher priority to come before things like "cd cd foo"
priority = 900

15
thefuck/rules/git_add.py Normal file
View File

@@ -0,0 +1,15 @@
import re
def match(command, settings):
return ('git' in command.script
and 'did not match any file(s) known to git.' in command.stderr
and "Did you forget to 'git add'?" in command.stderr)
def get_new_command(command, settings):
missing_file = re.findall(
r"error: pathspec '([^']*)' "
"did not match any file\(s\) known to git.", command.stderr)[0]
return 'git add -- {} && {}'.format(missing_file, command.script)

View File

@@ -0,0 +1,15 @@
import re
def match(command, settings):
return ('git' in command.script
and 'did not match any file(s) known to git.' in command.stderr
and "Did you forget to 'git add'?" not in command.stderr)
def get_new_command(command, settings):
missing_file = re.findall(
r"error: pathspec '([^']*)' "
"did not match any file\(s\) known to git.", command.stderr)[0]
return 'git branch {} && {}'.format(missing_file, command.script)

View File

@@ -0,0 +1,9 @@
def match(command, settings):
return (command.script.startswith(u'man')
and u'command not found' in command.stderr.lower())
def get_new_command(command, settings):
return u'man {}'.format(command.script[3:])
priority = 2000

View File

@@ -31,3 +31,6 @@ def get_new_command(command, settings):
new_command = get_close_matches(old_command,
_get_all_bins())[0]
return ' '.join([new_command] + command.script.split(' ')[1:])
priority = 3000

43
thefuck/rules/pacman.py Normal file
View File

@@ -0,0 +1,43 @@
import subprocess
from thefuck.utils import DEVNULL
def __command_available(command):
try:
subprocess.check_output([command], stderr=DEVNULL)
return True
except subprocess.CalledProcessError:
# command exists but is not happy to be called without any argument
return True
except OSError:
return False
def __get_pkgfile(command):
try:
return subprocess.check_output(
['pkgfile', '-b', '-v', command.script.split(" ")[0]],
universal_newlines=True, stderr=subprocess.DEVNULL
).split()
except subprocess.CalledProcessError:
return None
def match(command, settings):
return 'not found' in command.stderr and __get_pkgfile(command)
def get_new_command(command, settings):
package = __get_pkgfile(command)[0]
return '{} -S {} && {}'.format(pacman, package, command.script)
if not __command_available('pkgfile'):
enabled_by_default = False
elif __command_available('yaourt'):
pacman = 'yaourt'
elif __command_available('pacman'):
pacman = 'sudo pacman'
else:
enabled_by_default = False

View File

@@ -8,9 +8,7 @@ from subprocess import Popen, PIPE
from time import time
import os
from psutil import Process
FNULL = open(os.devnull, 'w')
from .utils import DEVNULL
class Generic(object):
@@ -58,7 +56,7 @@ class Bash(Generic):
return name, value
def _get_aliases(self):
proc = Popen('bash -ic alias', stdout=PIPE, stderr=FNULL, shell=True)
proc = Popen('bash -ic alias', stdout=PIPE, stderr=DEVNULL, shell=True)
return dict(
self._parse_alias(alias)
for alias in proc.stdout.read().decode('utf-8').split('\n')
@@ -80,7 +78,7 @@ class Zsh(Generic):
return name, value
def _get_aliases(self):
proc = Popen('zsh -ic alias', stdout=PIPE, stderr=FNULL, shell=True)
proc = Popen('zsh -ic alias', stdout=PIPE, stderr=DEVNULL, shell=True)
return dict(
self._parse_alias(alias)
for alias in proc.stdout.read().decode('utf-8').split('\n')

View File

@@ -4,7 +4,8 @@ from collections import namedtuple
Command = namedtuple('Command', ('script', 'stdout', 'stderr'))
Rule = namedtuple('Rule', ('name', 'match', 'get_new_command',
'enabled_by_default', 'side_effect'))
'enabled_by_default', 'side_effect',
'priority'))
class RulesNamesList(list):

View File

@@ -4,6 +4,9 @@ import six
from .types import Command
DEVNULL = open(os.devnull, 'w')
def which(program):
"""Returns `program` path or `None`."""

6
tox.ini Normal file
View File

@@ -0,0 +1,6 @@
[tox]
envlist = py27,py33,py34
[testenv]
deps = -rrequirements.txt
commands = py.test