├── LICENSE ├── README.md ├── cookiecutter.json ├── hooks ├── post_gen_project.py └── pre_gen_project.py └── {{cookiecutter.repo_name}} ├── .coveragerc ├── .dockerignore ├── .editorconfig ├── .github └── workflows │ ├── publish-docker.yml │ └── test.yml ├── .gitignore ├── .pre-commit-config.yaml ├── Dockerfile ├── Pipfile ├── README.md ├── setup.cfg ├── test ├── __init__.py └── test_{{cookiecutter.repo_name}}.py └── {{cookiecutter.repo_name}} ├── __init__.py ├── __main__.py └── {{cookiecutter.repo_name}}.py /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Sourcery 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Python Best Practices Cookiecutter 2 | 3 | Best practices [cookiecutter](https://github.com/audreyr/cookiecutter) template as described in this [blogpost](https://sourcery.ai/blog/python-best-practices/). 4 | 5 | ## Features 6 | - Testing with [pytest](https://docs.pytest.org/en/latest/) 7 | - Formatting with [black](https://github.com/psf/black) 8 | - Import sorting with [isort](https://github.com/timothycrosley/isort) 9 | - Static typing with [mypy](http://mypy-lang.org/) 10 | - Linting with [flake8](http://flake8.pycqa.org/en/latest/) 11 | - Git hooks that run all the above with [pre-commit](https://pre-commit.com/) 12 | - Deployment ready with [Docker](https://docker.com/) 13 | - Continuous Integration with [GitHub Actions](https://github.com/features/actions) 14 | 15 | ## Quickstart 16 | ```sh 17 | # Install pipx if pipenv and cookiecutter are not installed 18 | python3 -m pip install pipx 19 | python3 -m pipx ensurepath 20 | 21 | # Install pipenv using pipx 22 | pipx install pipenv 23 | 24 | # Use cookiecutter to create project from this template 25 | pipx run cookiecutter gh:sourcery-ai/python-best-practices-cookiecutter 26 | 27 | # Enter project directory 28 | cd 29 | 30 | # Initialise git repo 31 | git init 32 | 33 | # Install dependencies 34 | pipenv install --dev 35 | 36 | # Setup pre-commit and pre-push hooks 37 | pipenv run pre-commit install -t pre-commit 38 | pipenv run pre-commit install -t pre-push 39 | ``` 40 | -------------------------------------------------------------------------------- /cookiecutter.json: -------------------------------------------------------------------------------- 1 | { 2 | "project_name": "Best Practices", 3 | "repo_name": "{{ cookiecutter.project_name.lower().replace(' ', '_').replace('-', '_') }}" 4 | } 5 | -------------------------------------------------------------------------------- /hooks/post_gen_project.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | 5 | def set_python_version(): 6 | python_version = str(sys.version_info.major) + "." + str(sys.version_info.minor) 7 | 8 | file_names = ["Dockerfile", "Pipfile", ".github/workflows/test.yml"] 9 | for file_name in file_names: 10 | with open(file_name) as f: 11 | contents = f.read() 12 | contents = contents.replace(r"{python_version}", python_version) 13 | with open(file_name, "w") as f: 14 | f.write(contents) 15 | 16 | 17 | SUCCESS = "\x1b[1;32m" 18 | INFO = "\x1b[1;33m" 19 | TERMINATOR = "\x1b[0m" 20 | 21 | 22 | def main(): 23 | set_python_version() 24 | print(SUCCESS + "Project successfully initialized" + TERMINATOR) 25 | 26 | 27 | if __name__ == "__main__": 28 | main() 29 | -------------------------------------------------------------------------------- /hooks/pre_gen_project.py: -------------------------------------------------------------------------------- 1 | import re 2 | import sys 3 | 4 | MODULE_REGEX = r"^[a-zA-Z][_a-zA-Z0-9]+$" 5 | module_name = "{{ cookiecutter.repo_name }}" 6 | 7 | if not re.match(MODULE_REGEX, module_name): 8 | print("ERROR: %s is not a valid Python module name!" % module_name) 9 | sys.exit(1) 10 | -------------------------------------------------------------------------------- /{{cookiecutter.repo_name}}/.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | source = {{cookiecutter.repo_name}} 3 | omit = {{cookiecutter.repo_name}}/__main__.py 4 | 5 | [report] 6 | exclude_lines = 7 | # Have to re-enable the standard pragma 8 | pragma: no cover 9 | 10 | # Don't complain about missing debug-only code: 11 | def __repr__ 12 | if self\.debug 13 | 14 | # Don't complain if tests don't hit defensive assertion code: 15 | raise AssertionError 16 | raise NotImplementedError 17 | 18 | # Don't complain if non-runnable code isn't run: 19 | if 0: 20 | if __name__ == .__main__.: 21 | -------------------------------------------------------------------------------- /{{cookiecutter.repo_name}}/.dockerignore: -------------------------------------------------------------------------------- 1 | # Ignore everything 2 | ** 3 | 4 | # ... except for dependencies and source 5 | !Pipfile 6 | !Pipfile.lock 7 | !{{cookiecutter.repo_name}} 8 | -------------------------------------------------------------------------------- /{{cookiecutter.repo_name}}/.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org/#file-format-details 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | end_of_line = lf 7 | indent_size = 4 8 | indent_style = space 9 | insert_final_newline = true 10 | trim_trailing_whitespace = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | 15 | [Makefile] 16 | indent_style = tab 17 | -------------------------------------------------------------------------------- /{{cookiecutter.repo_name}}/.github/workflows/publish-docker.yml: -------------------------------------------------------------------------------- 1 | name: Publish docker image 2 | 3 | on: 4 | push: 5 | tags: "*" 6 | 7 | jobs: 8 | docker: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v2 12 | 13 | - name: Publish to GitHub packages 14 | uses: whoan/docker-build-with-cache-action@v2 15 | with: 16 | registry: docker.pkg.github.com 17 | username: ${{ "{{ github.actor }}" }} 18 | password: ${{ "{{ secrets.GITHUB_TOKEN }}" }} 19 | image_name: ${{ "{{ github.repository }}" }}/{{cookiecutter.repo_name}} 20 | -------------------------------------------------------------------------------- /{{cookiecutter.repo_name}}/.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | pull_request: {} 5 | push: 6 | branches: main 7 | tags: "*" 8 | 9 | jobs: 10 | test: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v2 14 | 15 | - name: Setup Python 16 | uses: actions/setup-python@v1 17 | with: 18 | python-version: {python_version} 19 | 20 | - name: Install dependencies with pipenv 21 | run: | 22 | pip install pipenv 23 | pipenv install --deploy --dev 24 | 25 | - run: pipenv run isort --recursive --diff . 26 | - run: pipenv run black --check . 27 | - run: pipenv run flake8 28 | - run: pipenv run mypy 29 | - run: pipenv run pytest --cov --cov-fail-under=100 30 | 31 | docker-image: 32 | runs-on: ubuntu-latest 33 | steps: 34 | - uses: actions/checkout@v2 35 | 36 | - name: Build docker image 37 | run: docker build . -t {{cookiecutter.repo_name}}:test 38 | 39 | - name: Smoke test docker image 40 | run: | 41 | docker run --rm {{cookiecutter.repo_name}}:test 10 42 | -------------------------------------------------------------------------------- /{{cookiecutter.repo_name}}/.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.gitignore.io/api/python 2 | # Edit at https://www.gitignore.io/?templates=python 3 | 4 | ### Python ### 5 | # Byte-compiled / optimized / DLL files 6 | __pycache__/ 7 | *.py[cod] 8 | *$py.class 9 | 10 | # C extensions 11 | *.so 12 | 13 | # Distribution / packaging 14 | .Python 15 | build/ 16 | develop-eggs/ 17 | dist/ 18 | downloads/ 19 | eggs/ 20 | .eggs/ 21 | lib/ 22 | lib64/ 23 | parts/ 24 | sdist/ 25 | var/ 26 | wheels/ 27 | pip-wheel-metadata/ 28 | share/python-wheels/ 29 | *.egg-info/ 30 | .installed.cfg 31 | *.egg 32 | MANIFEST 33 | 34 | # PyInstaller 35 | # Usually these files are written by a python script from a template 36 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 37 | *.manifest 38 | *.spec 39 | 40 | # Installer logs 41 | pip-log.txt 42 | pip-delete-this-directory.txt 43 | 44 | # Unit test / coverage reports 45 | htmlcov/ 46 | .tox/ 47 | .nox/ 48 | .coverage 49 | .coverage.* 50 | .cache 51 | nosetests.xml 52 | coverage.xml 53 | *.cover 54 | .hypothesis/ 55 | .pytest_cache/ 56 | 57 | # Translations 58 | *.mo 59 | *.pot 60 | 61 | # Django stuff: 62 | *.log 63 | local_settings.py 64 | db.sqlite3 65 | db.sqlite3-journal 66 | 67 | # Flask stuff: 68 | instance/ 69 | .webassets-cache 70 | 71 | # Scrapy stuff: 72 | .scrapy 73 | 74 | # Sphinx documentation 75 | docs/_build/ 76 | 77 | # PyBuilder 78 | target/ 79 | 80 | # Jupyter Notebook 81 | .ipynb_checkpoints 82 | 83 | # IPython 84 | profile_default/ 85 | ipython_config.py 86 | 87 | # pyenv 88 | .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # celery beat schedule file 98 | celerybeat-schedule 99 | 100 | # SageMath parsed files 101 | *.sage.py 102 | 103 | # Environments 104 | .env 105 | .venv 106 | env/ 107 | venv/ 108 | ENV/ 109 | env.bak/ 110 | venv.bak/ 111 | 112 | # Spyder project settings 113 | .spyderproject 114 | .spyproject 115 | 116 | # Rope project settings 117 | .ropeproject 118 | 119 | # mkdocs documentation 120 | /site 121 | 122 | # mypy 123 | .mypy_cache/ 124 | .dmypy.json 125 | dmypy.json 126 | 127 | # Pyre type checker 128 | .pyre/ 129 | 130 | # End of https://www.gitignore.io/api/python 131 | -------------------------------------------------------------------------------- /{{cookiecutter.repo_name}}/.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | # See https://pre-commit.com/ for usage and config 2 | repos: 3 | - repo: local 4 | hooks: 5 | - id: isort 6 | name: isort 7 | stages: [commit] 8 | language: system 9 | entry: pipenv run isort 10 | types: [python] 11 | 12 | - id: black 13 | name: black 14 | stages: [commit] 15 | language: system 16 | entry: pipenv run black 17 | types: [python] 18 | 19 | - id: flake8 20 | name: flake8 21 | stages: [commit] 22 | language: system 23 | entry: pipenv run flake8 24 | types: [python] 25 | exclude: setup.py 26 | 27 | - id: mypy 28 | name: mypy 29 | stages: [commit] 30 | language: system 31 | entry: pipenv run mypy 32 | types: [python] 33 | require_serial: true 34 | 35 | - id: pytest 36 | name: pytest 37 | stages: [commit] 38 | language: system 39 | entry: pipenv run pytest 40 | types: [python] 41 | pass_filenames: false 42 | 43 | - id: pytest-cov 44 | name: pytest 45 | stages: [push] 46 | language: system 47 | entry: pipenv run pytest --cov --cov-fail-under=100 48 | types: [python] 49 | pass_filenames: false 50 | -------------------------------------------------------------------------------- /{{cookiecutter.repo_name}}/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:{python_version}-slim AS base 2 | 3 | # Setup env 4 | ENV LANG C.UTF-8 5 | ENV LC_ALL C.UTF-8 6 | ENV PYTHONDONTWRITEBYTECODE 1 7 | ENV PYTHONFAULTHANDLER 1 8 | 9 | 10 | FROM base AS python-deps 11 | 12 | # Install pipenv and compilation dependencies 13 | RUN pip install pipenv 14 | RUN apt-get update && apt-get install -y --no-install-recommends gcc 15 | 16 | # Install python dependencies in /.venv 17 | COPY Pipfile . 18 | COPY Pipfile.lock . 19 | RUN PIPENV_VENV_IN_PROJECT=1 pipenv install --deploy 20 | 21 | 22 | FROM base AS runtime 23 | 24 | # Copy virtual env from python-deps stage 25 | COPY --from=python-deps /.venv /.venv 26 | ENV PATH="/.venv/bin:$PATH" 27 | 28 | # Create and switch to a new user 29 | RUN useradd --create-home appuser 30 | WORKDIR /home/appuser 31 | USER appuser 32 | 33 | # Install application into container 34 | COPY . . 35 | 36 | # Run the executable 37 | ENTRYPOINT ["python", "-m", "{{cookiecutter.repo_name}}"] 38 | CMD ["10"] 39 | -------------------------------------------------------------------------------- /{{cookiecutter.repo_name}}/Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | name = "pypi" 3 | url = "https://pypi.python.org/simple" 4 | verify_ssl = true 5 | 6 | [requires] 7 | python_version = "{python_version}" 8 | 9 | [packages] 10 | 11 | [dev-packages] 12 | black = "==22.3.0" 13 | flake8 = "*" 14 | isort = "*" 15 | mypy = "*" 16 | pre-commit = "*" 17 | pytest = "*" 18 | pytest-cov = "*" 19 | -------------------------------------------------------------------------------- /{{cookiecutter.repo_name}}/README.md: -------------------------------------------------------------------------------- 1 | # {{cookiecutter.project_name}} 2 | 3 | ## Setup 4 | ```sh 5 | # Install dependencies 6 | pipenv install --dev 7 | 8 | # Setup pre-commit and pre-push hooks 9 | pipenv run pre-commit install -t pre-commit 10 | pipenv run pre-commit install -t pre-push 11 | ``` 12 | 13 | ## Credits 14 | This package was created with Cookiecutter and the [sourcery-ai/python-best-practices-cookiecutter](https://github.com/sourcery-ai/python-best-practices-cookiecutter) project template. 15 | -------------------------------------------------------------------------------- /{{cookiecutter.repo_name}}/setup.cfg: -------------------------------------------------------------------------------- 1 | [flake8] 2 | ignore = E203, E266, E501, W503 3 | max-line-length = 88 4 | max-complexity = 18 5 | select = B,C,E,F,W,T4 6 | 7 | [isort] 8 | multi_line_output=3 9 | include_trailing_comma=True 10 | force_grid_wrap=0 11 | use_parentheses=True 12 | line_length=88 13 | 14 | [mypy] 15 | files={{cookiecutter.repo_name}},test 16 | ignore_missing_imports=true 17 | 18 | [tool:pytest] 19 | testpaths=test/ 20 | -------------------------------------------------------------------------------- /{{cookiecutter.repo_name}}/test/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sourcery-ai/python-best-practices-cookiecutter/96440e3b3fe6558ba55dfe8208398128dda9b644/{{cookiecutter.repo_name}}/test/__init__.py -------------------------------------------------------------------------------- /{{cookiecutter.repo_name}}/test/test_{{cookiecutter.repo_name}}.py: -------------------------------------------------------------------------------- 1 | from {{cookiecutter.repo_name}}.{{cookiecutter.repo_name}} import fib 2 | 3 | 4 | def test_fib() -> None: 5 | assert fib(0) == 0 6 | assert fib(1) == 1 7 | assert fib(2) == 1 8 | assert fib(3) == 2 9 | assert fib(4) == 3 10 | assert fib(5) == 5 11 | assert fib(10) == 55 12 | -------------------------------------------------------------------------------- /{{cookiecutter.repo_name}}/{{cookiecutter.repo_name}}/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sourcery-ai/python-best-practices-cookiecutter/96440e3b3fe6558ba55dfe8208398128dda9b644/{{cookiecutter.repo_name}}/{{cookiecutter.repo_name}}/__init__.py -------------------------------------------------------------------------------- /{{cookiecutter.repo_name}}/{{cookiecutter.repo_name}}/__main__.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | from {{cookiecutter.repo_name}}.{{cookiecutter.repo_name}} import fib 4 | 5 | if __name__ == "__main__": 6 | n = int(sys.argv[1]) 7 | print(fib(n)) 8 | -------------------------------------------------------------------------------- /{{cookiecutter.repo_name}}/{{cookiecutter.repo_name}}/{{cookiecutter.repo_name}}.py: -------------------------------------------------------------------------------- 1 | def fib(n: int) -> int: 2 | if n < 2: 3 | return n 4 | else: 5 | return fib(n - 1) + fib(n - 2) 6 | --------------------------------------------------------------------------------