├── .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 | [](https://travis-ci.org/collin5/precommit-hook)
8 | [](https://badge.fury.io/py/precommit-hook)
9 | [](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 | [](https://opensource.org/licenses/MIT)
28 |
29 | This project is licensed under MIT license. see the  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 |
--------------------------------------------------------------------------------