Open source check-indent to auto-yelpdent.

This commit is contained in:
William Ting 2016-04-29 15:51:32 -07:00 committed by William Ting
parent 35548254ad
commit 7f9dde14f0
9 changed files with 451 additions and 2 deletions

View file

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

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
name: autopep8 wrapper
description: "Runs autopep8 over python source. If you configure additional arguments you'll want to at least include -i."

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,11 +4,14 @@ project = pre_commit_hooks
envlist = py27,py33,py34,pypy,pypy3
[testenv]
deps = -rrequirements-dev.txt
deps =
-rrequirements-dev.txt
ipdb
ipython
passenv = HOME HOMEPATH PROGRAMDATA
commands =
coverage erase
coverage run -m pytest {posargs:tests}
coverage run -m pytest -s -rxs -vv {posargs:tests}
coverage report --show-missing --fail-under 100
pre-commit install -f --install-hooks
pre-commit run --all-files