├── .coveragerc ├── .gitignore ├── .travis.yml ├── LICENSE.md ├── MANIFEST.in ├── README.md ├── app ├── __init__.py ├── extras.py ├── precommit.hook └── scripts.py ├── requirements-dev.txt ├── requirements.txt ├── setup.py └── tests ├── test_precommit.py └── tests.py /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | source = app 3 | 4 | omit = 5 | */distutils/* 6 | */setuptools/* 7 | */ctypes/* 8 | */pkg_resources/* 9 | *getopt.py 10 | *numbers.py 11 | */virtualenv/* 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | 49 | # Translations 50 | *.mo 51 | *.pot 52 | 53 | # Django stuff: 54 | *.log 55 | local_settings.py 56 | 57 | # Flask stuff: 58 | instance/ 59 | .webassets-cache 60 | 61 | # Scrapy stuff: 62 | .scrapy 63 | 64 | # Sphinx documentation 65 | docs/_build/ 66 | 67 | # PyBuilder 68 | target/ 69 | 70 | # Jupyter Notebook 71 | .ipynb_checkpoints 72 | 73 | # pyenv 74 | .python-version 75 | 76 | # celery beat schedule file 77 | celerybeat-schedule 78 | 79 | # SageMath parsed files 80 | *.sage.py 81 | 82 | # dotenv 83 | .env 84 | 85 | # virtualenv 86 | .venv 87 | venv/ 88 | ENV/ 89 | bin/ 90 | include/ 91 | pip-selfcheck.json 92 | 93 | # Spyder project settings 94 | .spyderproject 95 | .spyproject 96 | 97 | # Rope project settings 98 | .ropeproject 99 | 100 | # mkdocs documentation 101 | /site 102 | 103 | # mypy 104 | .mypy_cache/ 105 | *.swp 106 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | { 2 | "language": "python", 3 | "python": "3.6.1", 4 | "install": [ 5 | "pip install -r requirements.txt" 6 | ], 7 | "script": [ 8 | "nosetests --with-coverage" 9 | ], 10 | "after_success": "coveralls", 11 | "cache": "pip", 12 | "group": "stable", 13 | "dist": "precise", 14 | "os": "linux" 15 | } 16 | 17 | 18 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Collins Abitekaniza 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE 2 | include requirements.txt 3 | include requirements-dev.txt 4 | include LICENSE 5 | include README.md 6 | 7 | recursive-include app * 8 | 9 | recursive-exclude tests * 10 | recursive-exclude * __pycache__ 11 | recursive-exclude * *.py[co] -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 |

3 | 4 |

5 | 6 | # Precommit hook 7 | [![Build Status](https://img.shields.io/travis/collin5/precommit-hook.svg?branch=master&logo=appveyor)](https://travis-ci.org/collin5/precommit-hook) 8 | [![PyPI version](https://badge.fury.io/py/precommit-hook.svg)](https://badge.fury.io/py/precommit-hook) 9 | [![Say Thanks!](https://img.shields.io/badge/SayThanks.io-%E2%98%BC-1EAEDB.svg)](https://saythanks.io/to/collin5) 10 | 11 | Automatically check your python code on every commit. 12 | 13 | ## Getting started 14 | Inside a git repository, do 15 | ```bash 16 | $ pip install precommit-hook 17 | ``` 18 | This will automatically add a hook to your repository that will automatically check your code everytime you make a commit. 19 | 20 | ## Add to requirements 21 | If you want to hook on every `pip install -r requirements.txt`, just add `precommit-hook` to your requirements with 22 | ```bash 23 | $ pip freeze > requirements.txt 24 | ``` 25 | 26 | ## License 27 | [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT) 28 | 29 | This project is licensed under MIT license. see the ![LICENSE.md](LICENSE.md) file for details 30 | 31 | 32 | ## Oh, Thanks! 33 | 34 | By the way... thank you! And if you'd like to [say thanks](https://saythanks.io/to/collin5)... :) 35 | 36 | ✨🍰✨ -------------------------------------------------------------------------------- /app/__init__.py: -------------------------------------------------------------------------------- 1 | # @Author: collins 2 | # @Date: 2017-06-14T15:44:53+03:00 3 | # @Last modified by: collins 4 | # @Last modified time: 2017-06-16T13:08:50+03:00 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /app/extras.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | template = '''#!/usr/bin/env python 5 | 6 | import os 7 | import sys 8 | import subprocess 9 | import configparser 10 | 11 | # available settings list 12 | AVAILABLE_SETTINGS = ( 13 | 'exclude', 'filename', 'select', 'ignore', 'max-line-length', 'count', 'config', 14 | 'quiet', 'show-pep8', 'show-source', 'statistics', 'verbose', 'max-complexity' 15 | ) 16 | 17 | SETTINGS_WITH_PARAMS = ( 18 | 'exclude', 'filename', 'select', 'ignore', 'max-line-length', 'format' 19 | ) 20 | 21 | # colorize output 22 | COLOR = { 23 | 'red': '\\033[1;31m', 24 | 'green': '\\033[1;32m', 25 | 'yellow': '\\033[1;33m', 26 | } 27 | 28 | 29 | def parse_settings(config_file): 30 | """ 31 | Get pep8 and flake8 lint settings from config file. 32 | Useful for define per-project lint options. 33 | """ 34 | settings = {'pep8': list(), 'flake8': list()} 35 | # read project settings 36 | if not os.path.exists(config_file) or not os.path.isfile(config_file): 37 | return settings 38 | try: 39 | config = configparser.ConfigParser() 40 | config.read(config_file) 41 | except configparser.MissingSectionHeaderError as e: 42 | print("ERROR: project lint config file is broken:\\n") 43 | print(repr(e)) 44 | sys.exit(1) 45 | # read project lint settings for pep8 and flake8 46 | for linter in settings.keys(): 47 | try: 48 | for key, value in config.items(linter): 49 | if key in AVAILABLE_SETTINGS: 50 | if key in SETTINGS_WITH_PARAMS: 51 | settings[linter].append("--{}={}".format(key, value)) 52 | else: 53 | settings[linter].append("--{}".format(key)) 54 | else: 55 | print("WARNING: unknown {} linter config: {}".format(linter, key)) 56 | except configparser.NoSectionError: 57 | pass 58 | return settings 59 | 60 | 61 | def system(*args, **kwargs): 62 | """ 63 | Run system command. 64 | """ 65 | kwargs.setdefault('stdout', subprocess.PIPE) 66 | proc = subprocess.Popen(args, **kwargs) 67 | out, err = proc.communicate() 68 | return out 69 | 70 | 71 | def get_changed_files(): 72 | """ 73 | Get python files from 'files to commit' git cache list. 74 | """ 75 | files = [] 76 | filelist = system('git', 'diff', '--cached', '--name-status').strip() 77 | for line in str(filelist.decode('utf-8')).split('\\n'): 78 | try: 79 | action, filename = line.strip().split() 80 | if filename.endswith('.py') and action != 'D': 81 | files.append(filename) 82 | except Exception as ex: 83 | pass 84 | return files 85 | 86 | 87 | def lint(cmd, files, settings): 88 | """ 89 | Run pep8 or flake8 lint. 90 | """ 91 | if cmd not in ('pep8', 'flake8'): 92 | raise Exception("Unknown lint command: {}".format(cmd)) 93 | args = settings[:] 94 | args.insert(0, cmd) 95 | args.extend(files) 96 | return str(system(*args).decode('utf-8')).strip().split('\\n') 97 | 98 | 99 | def main(): 100 | """ 101 | Do work 102 | """ 103 | files = get_changed_files() 104 | if not files: 105 | print("Python lint: {}SKIP \033[m".format(COLOR['yellow'])) 106 | return 107 | config_file = os.path.join(os.path.abspath(os.curdir), '.flake8') 108 | settings = parse_settings(config_file) 109 | errors = lint('flake8', files, settings['flake8']) 110 | 111 | if not len(errors) or errors[0] is '': 112 | print("Python lint: {}OK \033[m".format(COLOR['green'])) 113 | return 114 | print("Python lint: {}FAIL".format(COLOR['red'])) 115 | print("\\n".join(sorted(errors))) 116 | print("\033[m") 117 | print("Aborting commit due to python lint errors.") 118 | sys.exit(1) 119 | 120 | 121 | if __name__ == '__main__': 122 | main() 123 | 124 | 125 | ''' 126 | -------------------------------------------------------------------------------- /app/precommit.hook: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | from app.scripts import Exec 5 | 6 | if __name__ == '__main__': 7 | Exec.add_pre_commit() 8 | -------------------------------------------------------------------------------- /app/scripts.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | from setuptools.command.install import install 5 | from app.extras import template 6 | import stat 7 | import os 8 | import sys 9 | 10 | 11 | class Exec: 12 | @staticmethod 13 | def add_pre_commit(path=os.environ["PWD"]): 14 | # if .git/hooks directory does not exist (which means a non valid git repo) 15 | if not os.path.isdir(os.path.join(path, ".git/hooks")): 16 | message = '*****************************************************************\n' 17 | message += '* Oops, this hook can only be installed on a local GIT repository\n' 18 | message += '* Please, make sure to do a "git init" on this folder.' 19 | print(message) 20 | sys.exit(1) 21 | 22 | # Ok, it is a GIT repository... 23 | with open('{}/.git/hooks/pre-commit'.format(path), 'wb') as f: 24 | f.write(template.encode()) 25 | # Adding executable flag to the file 26 | st = os.stat('{}/.git/hooks/pre-commit'.format(path)) 27 | os.chmod('{}/.git/hooks/pre-commit'.format(path), st.st_mode | stat.S_IEXEC) 28 | print("Precommit script added successfully, continuing ...") 29 | return True 30 | 31 | 32 | class Post_install(install): 33 | def run(self): 34 | install.run(self) 35 | Exec.add_pre_commit() 36 | -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | certifi==2019.11.28 2 | chardet==3.0.4 3 | coverage==4.5.4 4 | coveralls==1.10.0 5 | docopt==0.6.2 6 | idna==2.8 7 | nose==1.3.7 8 | urllib3==1.25.7 9 | requests==2.23.0 10 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | flake8==3.7.8 2 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | from app.scripts import Post_install 5 | from setuptools import setup, find_packages 6 | import codecs 7 | from os import path 8 | 9 | here = path.abspath(path.dirname(__file__)) 10 | with codecs.open(path.join(here, 'README.md'), encoding='utf-8') as f: 11 | long_description = f.read() 12 | 13 | # Get the requirements from the requirements.txt file 14 | with codecs.open(path.join(here, 'requirements.txt'), encoding='utf-8') as f: 15 | requirements = f.read() 16 | 17 | # Get the dev requirements from the requirements-dev.txt file 18 | with codecs.open(path.join(here, 'requirements-dev.txt'), encoding='utf-8') as f: 19 | requirements_dev = f.read() 20 | 21 | setup( 22 | name="precommit-hook", 23 | version="0.2.1", 24 | author="Collins Abitekaniza", 25 | author_email="abtcolns@gmail.com", 26 | packages=find_packages(), 27 | include_package_data=True, 28 | license='MIT', 29 | url="https://github.com/collin5/precommit-hook", 30 | description="Auto check quality of python code before shipping", 31 | long_description=long_description, 32 | install_requires=requirements, 33 | platforms=['any'], 34 | extras_require={ 35 | 'dev': [requirements_dev.split('\n')] 36 | }, 37 | scripts=["app/precommit.hook"], 38 | cmdclass={ 39 | "install": Post_install, 40 | } 41 | ) 42 | -------------------------------------------------------------------------------- /tests/test_precommit.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | from tests import BaseTestCase 5 | from app.scripts import Exec 6 | import os 7 | 8 | 9 | class PreCommitTestCase(BaseTestCase): 10 | 11 | def test_add_precommit_successfully(self): 12 | action = Exec.add_pre_commit(os.path.join(os.getcwd(), "tmp")) 13 | self.assertTrue(action) 14 | 15 | def test_add_precommit_successfully_2(self): 16 | action = Exec.add_pre_commit(os.path.join(os.getcwd(), "tmp")) 17 | self.assertTrue(action) 18 | self.assertTrue(os.path.isfile("tmp/.git/hooks/pre-commit")) 19 | -------------------------------------------------------------------------------- /tests/tests.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | from unittest import TestCase 5 | import os 6 | 7 | 8 | class BaseTestCase(TestCase): 9 | 10 | def setUp(self): 11 | # create test temporary directory 12 | os.system("mkdir -p tmp && cd tmp && git init") # noqa 13 | 14 | def tearDown(self): 15 | # remove all test files 16 | os.system("rm -r tmp/") # noqa 17 | --------------------------------------------------------------------------------