Skip to content

Instantly share code, notes, and snippets.

@jeffryang24
Created June 25, 2018 09:36
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jeffryang24/af2257d5c8ba0b8c943834ca78e9e6f6 to your computer and use it in GitHub Desktop.
Save jeffryang24/af2257d5c8ba0b8c943834ca78e9e6f6 to your computer and use it in GitHub Desktop.
Git pre-commit hook to lint changed python files
#!/usr/bin/env python
"""
Lint python files before commit with flake8 and pep8 lint tools.
You can define lint settings in '.pep8' file in the project root.
Section '[pep8]' if for pep8 linter, and section '[flake8]' is for flake8.
INSTALL:
1. sudo pip install flake8 pep8
2. Save this file to '.git/hooks/pre-commit' file in your git repository
3. Enjoy
TODO:
- Remove pep8 lint after flake8 will be updated (E127, E128 not recognized now)
"""
import os
import sys
import subprocess
import ConfigParser
# available settings list
AVAILABLE_SETTINGS = (
'exclude', 'filename', 'select', 'ignore', 'max-line-length', 'count',
'quiet', 'show-pep8', 'show-source', 'statistics', 'verbose'
)
SETTINGS_WITH_PARAMS = (
'exclude', 'filename', 'select', 'ignore', 'max-line-length', 'format'
)
# colorize output
COLOR = {
'red': '\033[1;31m',
'green': '\033[1;32m',
'yellow': '\033[1;33m',
'off': '\033[1;m'
}
def parse_settings(config_file):
"""
Get pep8 and flake8 lint settings from config file.
Useful for define per-project lint options.
"""
settings = {'pep8': list(), 'flake8': list()}
# read project settings
if not os.path.exists(config_file) or not os.path.isfile(config_file):
return settings
try:
config = ConfigParser.ConfigParser()
config.read(config_file)
except ConfigParser.MissingSectionHeaderError, e:
print "ERROR: project lint config file is broken:\n"
print repr(e)
sys.exit(1)
# read project lint settings for pep8 and flake8
for linter in settings.keys():
try:
for key, value in config.items(linter):
if key in AVAILABLE_SETTINGS:
if key in SETTINGS_WITH_PARAMS:
settings[linter].append("--%s=%s" % (key, value))
else:
settings[linter].append("--%s" % key)
else:
print "WARNING: unknown %s linter config: %s" % (
linter, key)
except ConfigParser.NoSectionError:
pass
return settings
def system(*args, **kwargs):
"""
Run system command.
"""
kwargs.setdefault('stdout', subprocess.PIPE)
proc = subprocess.Popen(args, **kwargs)
out, err = proc.communicate()
return out
def get_changed_files():
"""
Get python files from 'files to commit' git cache list.
"""
files = []
filelist = system('git', 'diff', '--cached', '--name-status').strip()
for line in filelist.split('\n'):
action, filename = line.strip().split()
if filename.endswith('.py') and action != 'D':
files.append(filename)
return files
def lint(cmd, files, settings):
"""
Run pep8 or flake8 lint.
"""
if cmd not in ('pep8', 'flake8'):
raise Exception("Unknown lint command: %s" % cmd)
args = settings[:]
args.insert(0, cmd)
args.extend(files)
return filter(None, system(*args).strip().split('\n'))
def main():
"""
Do work
"""
files = get_changed_files()
if not files:
print "Python lint: %(yellow)sSKIP%(off)s" % COLOR
return
config_file = os.path.join(os.path.abspath(os.curdir), '.pep8')
settings = parse_settings(config_file)
errors = lint('flake8', files, settings['flake8'])
# TODO: remove pep8 lint when flake8 will be updated (see E127, E128)
for err in lint('pep8', files, settings['pep8']):
divider = err.find(' ', err.find(' ') + 1)
pep8_error = err[0:divider]
if not any(error[0:divider] == pep8_error for error in errors):
errors.append(err)
if not errors:
print "Python lint: %(green)sOK%(off)s" % COLOR
return
print "Python lint: %(red)sFAIL%(off)s" % COLOR
print
print "\n".join(sorted(errors))
print
print "Aborting commit due to python lint errors."
sys.exit(1)
if __name__ == '__main__':
main()
@klieret
Copy link

klieret commented Dec 19, 2020

Thanks a lot for publishing this, was very helpful.

Note however that this will fail for renames, line 93 should be changed to

action = line.strip().split()[0]
filenames = line.strip().split()[1:]

etc.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment