This commit is contained in:
William Ting 2017-07-05 19:30:03 +00:00 committed by GitHub
commit 2e63227bc1
9 changed files with 450 additions and 3 deletions

View file

@ -23,6 +23,7 @@ Add this to your `.pre-commit-config.yaml`
### Hooks available ### Hooks available
- `auto-indent` - Checks and fixes indentation of function calls.
- `autopep8-wrapper` - Runs autopep8 over python source. - `autopep8-wrapper` - Runs autopep8 over python source.
- Ignore PEP 8 violation types with `args: ['-i', '--ignore=E000,...']` or - Ignore PEP 8 violation types with `args: ['-i', '--ignore=E000,...']` or
through configuration of the `[pycodestyle]` section in through configuration of the `[pycodestyle]` section in

View file

@ -1,3 +1,9 @@
- id: auto-indent
name: Check and fix indentation
language: python
entry: auto-indent
description: "Checks and fixes indentation of things like parameters and arguments."
files: \.py$
- id: autopep8-wrapper - id: autopep8-wrapper
language: system language: system
name: upgrade-your-pre-commit-version name: upgrade-your-pre-commit-version

View file

@ -0,0 +1,183 @@
# -*- coding: utf-8 -*-
from __future__ import absolute_import
from __future__ import print_function
import argparse
import ast
import io
import re
import textwrap
import traceback
from operator import itemgetter
RETURN_CODE = dict(
no_change=0,
fixed_file=1,
syntax_error=2,
)
class IndentNodeVisitor(ast.NodeVisitor):
def __init__(self):
self.lines = []
def visit_Call(self, node):
args = list(node.args)
if node.keywords:
args.extend([keyword.value for keyword in node.keywords])
if node.starargs:
args.append(node.starargs)
if node.kwargs:
args.append(node.kwargs)
try:
if args and args[0].lineno == node.lineno:
for arg in args:
if arg.lineno != node.lineno:
self.lines.append({
'line': node.lineno,
'nextline': arg.lineno,
'column': args[0].col_offset,
})
break
except AttributeError:
# Python 3.3 is not supported because line numbers were
# removed from arg objects (compared to Python 2.7 and Python 3.4):
# https://docs.python.org/3.3/library/ast.html
pass
finally:
self.generic_visit(node)
def visit_ClassDef(self, node):
if node.bases and node.bases[0].lineno != node.lineno:
self.generic_visit(node)
return
for base in node.bases:
if base.lineno != node.lineno:
self.lines.append({
'line': node.lineno,
'nextline': base.lineno,
'column': node.bases[0].col_offset,
})
break
self.generic_visit(node)
def visit_FunctionDef(self, node):
try:
if node.args.args and node.args.args[0].lineno != node.lineno:
self.generic_visit(node)
return
for arg in node.args.args:
if arg.lineno != node.lineno:
self.lines.append({
'line': node.lineno,
'nextline': arg.lineno,
'column': node.args.args[0].col_offset,
})
break
except AttributeError:
# Python 3.3 is not supported because line numbers were
# removed from arg objects (compared to Python 2.7 and Python 3.4):
# https://docs.python.org/3.3/library/ast.html
pass
finally:
self.generic_visit(node)
def check_files(files):
rc = RETURN_CODE['no_change']
for fpath in files:
with open(fpath) as f:
source = f.read()
try:
tree = ast.parse(source, fpath)
except SyntaxError:
traceback.print_exc()
return RETURN_CODE['syntax_error']
visitor = IndentNodeVisitor()
visitor.visit(tree)
if visitor.lines:
rc = RETURN_CODE['fixed_file']
print('Fixing ' + fpath)
fix_lines(
file_path=fpath,
source_lines=source.split('\n'),
line_data=visitor.lines,
)
return rc
def fix_lines(file_path, source_lines, line_data):
line_data = sorted(line_data, key=itemgetter('line'))
last_lineno = None
same_line_adjustment = 0
for i, data in enumerate(line_data):
adjustment = 0
lineno = data['line'] + i - 1
if data['line'] == last_lineno:
adjustment = same_line_adjustment
start = 1 + source_lines[lineno].rfind(
'(',
0,
data['column'] + adjustment,
)
nextline = source_lines[data['nextline'] + i - 1]
indent = re.search('[ \t]*', nextline).group()
new_line = indent + source_lines[lineno][start:]
source_lines[lineno] = source_lines[lineno][:start]
source_lines.insert(lineno + 1, new_line)
with io.open(file_path, 'w') as f:
f.write(u'\n'.join(source_lines))
last_lineno = data['line']
same_line_adjustment = len(indent) - start
def main(argv=None):
parser = argparse.ArgumentParser(
description=textwrap.dedent(
"""
Checks that parameters and arguments to functions, classes,
and function calls are either all on the same line as the
corresponding function or class or that none are.
If it finds any violations, the offending parameters or arguments
will automatically be moved to a new line.
Return codes are:
0: no file change
1: file fixed
2: Python syntax error
"""
)
)
parser.add_argument('filenames', nargs='*')
args = parser.parse_args(argv)
return check_files(args.filenames)
if __name__ == '__main__':
exit(main())

View file

@ -0,0 +1,62 @@
# bad
def foo(
a,
b,
c,
*args,
**kwargs
):
pass
# okay
def bar():
pass
# bad
_ = foo(
1,
2,
3,
4,
e=5,
*[],
**{}
)
# okay
_ = bar()
# okay
_ = foo(bar, baz, womp)
# bad
class baz(
object,
object,
):
pass
# okay
class qux:
pass
# bad
_ = foo(
foo(
1 + foo(
1, # trickier
2,
3,
),
4,
5,
),
6,
7,
)
# bad
_ = '(wow)'.foo(
no,
way,
)

View file

@ -0,0 +1,55 @@
# bad
def foo(a,
b,
c,
*args,
**kwargs
):
pass
# okay
def bar():
pass
# bad
_ = foo(1,
2,
3,
4,
e=5,
*[],
**{}
)
# okay
_ = bar()
# okay
_ = foo(bar, baz, womp)
# bad
class baz(object,
object,
):
pass
# okay
class qux:
pass
# bad
_ = foo(foo(1 + foo(1, # trickier
2,
3,
),
4,
5,
),
6,
7,
)
# bad
_ = '(wow)'.foo(no,
way,
)

View file

@ -0,0 +1,61 @@
# bad
def foo(a,
b,
c,
*args,
**kwargs
):
pass
# okay
def bar():
pass
# bad
_ = foo(
1,
2,
3,
4,
e=5,
*[],
**{}
)
# okay
_ = bar()
# okay
_ = foo(bar, baz, womp)
# bad
class baz(
object,
object,
):
pass
# okay
class qux:
pass
# bad
_ = foo(
foo(
1 + foo(
1, # trickier
2,
3,
),
4,
5,
),
6,
7,
)
# bad
_ = '(wow)'.foo(
no,
way,
)

View file

@ -0,0 +1,4 @@
def foo(a,
b,
)
pass

74
tests/auto_indent_test.py Normal file
View file

@ -0,0 +1,74 @@
# -*- coding: utf-8 -*-
from __future__ import absolute_import
from __future__ import print_function
import filecmp
import os
import sys
from shutil import copyfile
import pytest
from pre_commit_hooks.auto_indent import main
from pre_commit_hooks.auto_indent import RETURN_CODE
from testing.util import get_resource_path
def _has_ast_arg_lineno_support():
"""Checks if AST has all the info necessary to auto indent.
Some Python implementations removed line numbers from AST arg objects and
as a result we can't automatically format function calls.
"""
if sys.version_info.major == 3 and sys.version_info.minor == 3:
return False
elif sys.version_info.major == 3 and sys.version_info.minor == 2:
return False
else:
return True
@pytest.mark.parametrize(
('infile', 'expected_rc'),
(
('file_with_acceptable_indentation', RETURN_CODE['no_change']),
('syntax_error', RETURN_CODE['syntax_error']),
)
)
def test_check_indentation(infile, expected_rc):
in_path = get_resource_path(os.path.join('auto_indent', infile))
assert main([in_path]) == expected_rc
@pytest.mark.skipif(not _has_ast_arg_lineno_support(), reason='See docstring.')
def test_fix_bad_indentation(tmpdir):
in_path = get_resource_path(os.path.join(
'auto_indent',
'file_with_bad_indentation'
))
expected_out_path = get_resource_path(os.path.join(
'auto_indent',
'file_with_acceptable_indentation'
))
tmp_path = tmpdir.join('auto_indent').strpath
copyfile(in_path, tmp_path)
assert main([tmp_path]) == RETURN_CODE['fixed_file']
assert filecmp.cmp(tmp_path, expected_out_path)
@pytest.mark.skipif(_has_ast_arg_lineno_support(), reason='See docstring.')
def test_partial_fix_bad_indentation(tmpdir):
in_path = get_resource_path(os.path.join(
'auto_indent',
'file_with_bad_indentation'
))
expected_out_path = get_resource_path(os.path.join(
'auto_indent',
'file_with_partial_indentation'
))
tmp_path = tmpdir.join('auto_indent').strpath
copyfile(in_path, tmp_path)
assert main([tmp_path]) == RETURN_CODE['fixed_file']
assert filecmp.cmp(tmp_path, expected_out_path)

View file

@ -4,7 +4,8 @@ project = pre_commit_hooks
envlist = py27,py35,py36,pypy envlist = py27,py35,py36,pypy
[testenv] [testenv]
deps = -rrequirements-dev.txt deps =
-rrequirements-dev.txt
passenv = HOME HOMEPATH PROGRAMDATA passenv = HOME HOMEPATH PROGRAMDATA
setenv = setenv =
GIT_AUTHOR_NAME = "test" GIT_AUTHOR_NAME = "test"
@ -13,8 +14,8 @@ setenv =
GIT_COMMITTER_EMAIL = "test@example.com" GIT_COMMITTER_EMAIL = "test@example.com"
commands = commands =
coverage erase coverage erase
coverage run -m pytest {posargs:tests} coverage run --source=pre_commit_hooks/ -m pytest -rxs --durations 10 {posargs:tests}
coverage report --show-missing --fail-under 100 coverage report --show-missing --fail-under 99
pre-commit install -f --install-hooks pre-commit install -f --install-hooks
pre-commit run --all-files pre-commit run --all-files