├── .gitattributes ├── {{cookiecutter.project_name}} ├── tests │ ├── unit │ │ └── __init__.py │ ├── integration │ │ └── __init__.py │ ├── __init__.py │ └── test_main.py ├── src │ └── {{cookiecutter.package_name}} │ │ ├── py.typed │ │ ├── __init__.py │ │ └── __main__.py ├── dev_configurations │ ├── .cookiecutter.json │ ├── .flake8 │ └── .pre-commit-config.yaml ├── .dockerignore ├── docs │ ├── README.md │ ├── source │ │ ├── usage.rst │ │ ├── index.rst │ │ └── conf.py │ ├── Makefile │ └── make.bat ├── .vscode │ ├── extensions.json │ └── settings.json ├── docker-compose.yaml ├── .github │ └── workflows │ │ └── linting_and_type_checking.yaml ├── Dockerfile ├── pyproject.toml ├── scripts │ └── setup_host_env.sh ├── README.md └── noxfile.py ├── .gitignore ├── docs ├── codeofconduct.md ├── license.md ├── requirements.txt ├── contributing.md ├── conf.py ├── index.md ├── quickstart.md └── guide.md ├── .readthedocs.yml ├── .github ├── workflows │ ├── constraints.txt │ ├── release-drafter.yml │ ├── labeler.yml │ ├── docs.yml │ ├── pre-commit.yml │ ├── update-instance.yml │ └── tests.yml ├── release-drafter.yml ├── dependabot.yml └── labels.yml ├── .pre-commit-config.yaml ├── hooks └── post_gen_project.py ├── cookiecutter.json ├── LICENSE ├── tools ├── dependencies-table.py ├── publish-github-release.py └── prepare-github-release.py ├── noxfile.py ├── CONTRIBUTING.md ├── README.md └── CODE_OF_CONDUCT.md /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/tests/unit/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.nox/ 2 | /docs/build/ 3 | __pycache__/ 4 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/tests/integration/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/codeofconduct.md: -------------------------------------------------------------------------------- 1 | ```{include} ../CODE_OF_CONDUCT.md 2 | 3 | ``` 4 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/src/{{cookiecutter.package_name}}/py.typed: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/dev_configurations/.cookiecutter.json: -------------------------------------------------------------------------------- 1 | {{ cookiecutter | jsonify }} 2 | -------------------------------------------------------------------------------- /docs/license.md: -------------------------------------------------------------------------------- 1 | # License 2 | 3 | ```{literalinclude} ../LICENSE 4 | --- 5 | language: none 6 | --- 7 | ``` 8 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/.dockerignore: -------------------------------------------------------------------------------- 1 | .coverage 2 | dist 3 | docs 4 | *.nox/ 5 | *.pytype/ 6 | __pycache__/ -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | furo==2022.12.7 2 | myst-parser==0.18.1 3 | sphinx==6.1.3 4 | sphinx-autobuild==2021.3.14 5 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/src/{{cookiecutter.package_name}}/__init__.py: -------------------------------------------------------------------------------- 1 | """{{cookiecutter.friendly_name}}.""" 2 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/tests/__init__.py: -------------------------------------------------------------------------------- 1 | """Test suite for the {{cookiecutter.package_name}} package.""" 2 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/docs/README.md: -------------------------------------------------------------------------------- 1 | need to run sphinx-apidoc -f -o docs/source src/ 2 | Then cd docs; make html -------------------------------------------------------------------------------- /docs/contributing.md: -------------------------------------------------------------------------------- 1 | ```{include} ../CONTRIBUTING.md 2 | --- 3 | end-before: 4 | --- 5 | ``` 6 | 7 | [code of conduct]: codeofconduct 8 | -------------------------------------------------------------------------------- /.readthedocs.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | sphinx: 3 | configuration: docs/conf.py 4 | formats: all 5 | python: 6 | version: 3.8 7 | install: 8 | - requirements: docs/requirements.txt 9 | -------------------------------------------------------------------------------- /.github/workflows/constraints.txt: -------------------------------------------------------------------------------- 1 | pip==23.0 2 | cookiecutter==2.1.1 3 | cutty==0.18.0 4 | nox==2022.11.21 5 | nox-poetry==1.0.2 6 | poetry==1.3.2 7 | pre-commit==3.0.4 8 | virtualenv==20.19.0 9 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/docs/source/usage.rst: -------------------------------------------------------------------------------- 1 | Usage 2 | ===== 3 | 4 | .. _installation: 5 | 6 | Installation 7 | ------------ 8 | 9 | To use {{cookiecutter.project_name}}, first: 10 | 11 | .. code-block:: console 12 | 13 | (.venv) $ some-code -------------------------------------------------------------------------------- /.github/workflows/release-drafter.yml: -------------------------------------------------------------------------------- 1 | name: Release Drafter 2 | on: 3 | push: 4 | branches: 5 | - main 6 | jobs: 7 | draft_release: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: release-drafter/release-drafter@v5.22.0 11 | env: 12 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 13 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/src/{{cookiecutter.package_name}}/__main__.py: -------------------------------------------------------------------------------- 1 | """Command-line interface.""" 2 | import click 3 | 4 | 5 | @click.command() 6 | @click.version_option() 7 | def main() -> None: 8 | """{{cookiecutter.friendly_name}}.""" 9 | 10 | 11 | if __name__ == "__main__": 12 | main(prog_name="{{cookiecutter.project_name}}") # pragma: no cover 13 | -------------------------------------------------------------------------------- /.github/workflows/labeler.yml: -------------------------------------------------------------------------------- 1 | name: Labeler 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | labeler: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Check out the repository 13 | uses: actions/checkout@v3 14 | 15 | - name: Run Labeler 16 | uses: crazy-max/ghaction-github-labeler@v4.1.0 17 | with: 18 | skip-delete: true 19 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | exclude: "^{{cookiecutter\\.project_name}}/" 2 | repos: 3 | - repo: https://github.com/pre-commit/pre-commit-hooks 4 | rev: v4.1.0 5 | hooks: 6 | - id: check-yaml 7 | - id: end-of-file-fixer 8 | - id: trailing-whitespace 9 | - id: check-added-large-files 10 | - repo: https://github.com/pre-commit/mirrors-prettier 11 | rev: v2.6.0 12 | hooks: 13 | - id: prettier 14 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/dev_configurations/.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | select = B,B9,C,D,DAR,E,F,N,RST,S,W 3 | ignore = B008,C901,D100,D104,D105,D200,D205,D400,D401,D404,D407,E203,E501,RST201,RST203,RST301,RST401,RST499,S104,S404,S602,S605,S607,W503 4 | max-line-length = 140 5 | max-complexity = 10 6 | ignore-decorators=overrides 7 | docstring-convention = numpy 8 | per-file-ignores = tests/*:S101 9 | rst-roles = class,const,func,meth,mod,ref 10 | rst-directives = deprecated 11 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "ms-python.vscode-pylance", 4 | "ms-python.python", 5 | "donjayamanne.python-extension-pack", 6 | "donjayamanne.python-environment-manager", 7 | "kevinrose.vsc-python-indent", 8 | "njpwerner.autodocstring", 9 | "visualstudioexptteam.vscodeintellicode", 10 | "ms-toolsai.jupyter", 11 | "ms-azuretools.vscode-docker", 12 | ] 13 | } -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/docs/source/index.rst: -------------------------------------------------------------------------------- 1 | .. {{cookiecutter.project_name}} documentation master file 2 | 3 | Welcome to {{cookiecutter.project_name}}'s documentation! 4 | =================================== 5 | 6 | .. toctree:: 7 | :maxdepth: 2 8 | :caption: Contents: 9 | 10 | usage 11 | modules 12 | 13 | 14 | Indices and tables 15 | ================== 16 | 17 | Check out the :doc:`usage` section for further information, including how to 18 | :ref:`install ` the project. 19 | 20 | * :ref:`genindex` 21 | * :ref:`modindex` 22 | * :ref:`search` -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/docs/source/conf.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', '..', 'src')) 5 | """Sphinx configuration.""" 6 | project = "{{cookiecutter.friendly_name}}" 7 | author = "{{cookiecutter.author}}" 8 | copyright = "{{cookiecutter.copyright_year}}, {{cookiecutter.author}}" 9 | extensions = [ 10 | "sphinx.ext.autodoc", 11 | "sphinx.ext.autosummary", 12 | "sphinx.ext.napoleon", 13 | "sphinx_click", 14 | ] 15 | autosummary_generate = True 16 | autosummary_imported_members = True 17 | autodoc_typehints = "description" 18 | html_theme = "furo" 19 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/tests/test_main.py: -------------------------------------------------------------------------------- 1 | """Test cases for the __main__ module.""" 2 | import pytest 3 | from click.testing import CliRunner 4 | 5 | from {{cookiecutter.package_name}} import __main__ 6 | 7 | 8 | @pytest.fixture 9 | def runner() -> CliRunner: 10 | """Fixture for invoking command-line interfaces. 11 | 12 | Returns 13 | ------- 14 | CliRunner 15 | _description_ 16 | """ 17 | return CliRunner() 18 | 19 | 20 | def test_main_succeeds(runner: CliRunner) -> None: 21 | """It exits with a status code of zero.""" 22 | result = runner.invoke(__main__.main) 23 | assert result.exit_code == 0 24 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = source 9 | BUILDDIR = build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /hooks/post_gen_project.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import json 3 | from pathlib import Path 4 | 5 | 6 | def reindent_cookiecutter_json(): 7 | """Indent .cookiecutter.json using two spaces. 8 | 9 | The jsonify extension distributed with Cookiecutter uses an indentation 10 | width of four spaces. This conflicts with the default indentation width of 11 | Prettier for JSON files. Prettier is run as a pre-commit hook in CI. 12 | """ 13 | path = Path("dev_configurations/.cookiecutter.json") 14 | 15 | with path.open() as io: 16 | data = json.load(io) 17 | 18 | with path.open(mode="w") as io: 19 | json.dump(data, io, sort_keys=True, indent=2) 20 | io.write("\n") 21 | 22 | 23 | if __name__ == "__main__": 24 | reindent_cookiecutter_json() 25 | -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | name: Check documentation 2 | on: [push, pull_request] 3 | jobs: 4 | docs: 5 | name: Build documentation & check links 6 | runs-on: ubuntu-latest 7 | steps: 8 | - uses: actions/checkout@v3 9 | - uses: actions/setup-python@v4 10 | with: 11 | python-version: "3.8" 12 | - run: | 13 | pip install --constraint=.github/workflows/constraints.txt pip 14 | pip install --constraint=.github/workflows/constraints.txt nox 15 | - name: Build documentation 16 | run: nox --force-color --session=docs 17 | - uses: actions/upload-artifact@v3 18 | with: 19 | name: docs 20 | path: docs/_build 21 | - name: Check links 22 | run: nox --force-color --session=linkcheck 23 | -------------------------------------------------------------------------------- /cookiecutter.json: -------------------------------------------------------------------------------- 1 | { 2 | "project_name": "my-python-project", 3 | "package_name": "{{ cookiecutter.project_name.replace('-', '_') }}", 4 | "friendly_name": "{{ cookiecutter.project_name.replace('-', ' ').title() }}", 5 | "author": "Tarmily Wen", 6 | "email": "tarmilywen@gmail", 7 | "github_user": "ChickenTarm", 8 | "version": "0.0.0", 9 | "copyright_year": "{% now 'utc', '%Y' %}", 10 | "development_status": [ 11 | "Development Status :: 1 - Planning", 12 | "Development Status :: 2 - Pre-Alpha", 13 | "Development Status :: 3 - Alpha", 14 | "Development Status :: 4 - Beta", 15 | "Development Status :: 5 - Production/Stable", 16 | "Development Status :: 6 - Mature", 17 | "Development Status :: 7 - Inactive" 18 | ], 19 | "_copy_without_render": [ 20 | "scripts/setup_host_env.sh" 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /.github/release-drafter.yml: -------------------------------------------------------------------------------- 1 | categories: 2 | - title: ":boom: Breaking Changes" 3 | label: "breaking" 4 | - title: ":rocket: Features" 5 | label: "enhancement" 6 | - title: ":fire: Removals and Deprecations" 7 | label: "removal" 8 | - title: ":beetle: Fixes" 9 | label: "bug" 10 | - title: ":racehorse: Performance" 11 | label: "performance" 12 | - title: ":rotating_light: Testing" 13 | label: "testing" 14 | - title: ":construction_worker: Continuous Integration" 15 | label: "ci" 16 | - title: ":books: Documentation" 17 | label: "documentation" 18 | - title: ":hammer: Refactoring" 19 | label: "refactoring" 20 | - title: ":lipstick: Style" 21 | label: "style" 22 | - title: ":package: Dependencies" 23 | labels: 24 | - "dependencies" 25 | - "build" 26 | exclude-labels: 27 | - "skip-changelog" 28 | template: | 29 | ## Changes 30 | 31 | $CHANGES 32 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: pip 4 | directory: "/docs" 5 | schedule: 6 | interval: daily 7 | time: "04:00" 8 | timezone: "Europe/Berlin" 9 | labels: 10 | - "cookiecutter" 11 | - "dependencies" 12 | - "python" 13 | open-pull-requests-limit: 99 14 | - package-ecosystem: pip 15 | directory: "/.github/workflows" 16 | schedule: 17 | interval: daily 18 | time: "04:00" 19 | timezone: "Europe/Berlin" 20 | labels: 21 | - "cookiecutter" 22 | - "dependencies" 23 | - "python" 24 | open-pull-requests-limit: 99 25 | - package-ecosystem: github-actions 26 | directory: "/" 27 | schedule: 28 | interval: daily 29 | time: "04:00" 30 | timezone: "Europe/Berlin" 31 | labels: 32 | - "cookiecutter" 33 | - "dependencies" 34 | - "github_actions" 35 | open-pull-requests-limit: 99 36 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=source 11 | set BUILDDIR=build 12 | 13 | %SPHINXBUILD% >NUL 2>NUL 14 | if errorlevel 9009 ( 15 | echo. 16 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 17 | echo.installed, then set the SPHINXBUILD environment variable to point 18 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 19 | echo.may add the Sphinx directory to PATH. 20 | echo. 21 | echo.If you don't have Sphinx installed, grab it from 22 | echo.https://www.sphinx-doc.org/ 23 | exit /b 1 24 | ) 25 | 26 | if "%1" == "" goto help 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | """Sphinx configuration.""" 2 | from datetime import datetime 3 | 4 | 5 | project = "Hypermodern Python Cookiecutter" 6 | author = "Claudio Jolowicz" 7 | copyright = f"{datetime.now().year}, {author}" 8 | extensions = ["sphinx.ext.intersphinx", "myst_parser"] 9 | intersphinx_mapping = {"mypy": ("https://mypy.readthedocs.io/en/stable/", None)} 10 | language = "en" 11 | html_theme = "furo" 12 | html_logo = "_static/logo.png" 13 | linkcheck_ignore = [ 14 | "codeofconduct.html", 15 | "https://github.com/PyCQA/flake8-bugbear#", 16 | "https://github.com/peterjc/flake8-rst-docstrings#", 17 | "https://github.com/pre-commit/pre-commit-hooks#", 18 | "https://github.com/pycqa/pep8-naming#", 19 | "https://github.com/terrencepreilly/darglint#", 20 | "https://github.com/PyCQA/mccabe#", 21 | "https://github.com/ChickenTarm/cookiecutter-python-ml-project/releases/tag/", 22 | "https://cookiecutter-hypermodern-python.readthedocs.io", 23 | "https://badgen.net/badge/status/alpha/d8624d", 24 | ] 25 | myst_enable_extensions = [ 26 | "colon_fence", 27 | "deflist", 28 | "substitution", 29 | ] 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright © 2020 Claudio Jolowicz 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 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "[python]":{ 3 | "editor.semanticHighlighting.enabled": true, 4 | }, 5 | "python.testing.pytestArgs": ["tests"], 6 | "python.testing.unittestEnabled": false, 7 | "python.testing.pytestEnabled": true, 8 | "python.languageServer": "Pylance", 9 | "python.analysis.typeCheckingMode": "basic", 10 | "python.analysis.diagnosticMode": "workspace", 11 | "python.analysis.autoImportCompletions": true, 12 | "python.analysis.useLibraryCodeForTypes": true, 13 | "python.analysis.diagnosticSeverityOverrides": { 14 | "reportGeneralTypeIssues": "warning", 15 | "reportPrivateImportUsage": "none", 16 | "reportUnboundVariable": "warning" 17 | }, 18 | "editor.semanticTokenColorCustomizations": { 19 | "[One Dark Pro]": { // Apply to this theme only 20 | "enabled": true, 21 | "rules": { 22 | "magicFunction:python": "#ee0000", 23 | "function.declaration:python": "#990000", 24 | "*.decorator:python": "#0000dd", 25 | "*.typeHint:python": "#5500aa", 26 | "*.typeHintComment:python": "#aaaaaa" 27 | } 28 | } 29 | }, 30 | } 31 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/docker-compose.yaml: -------------------------------------------------------------------------------- 1 | networks: 2 | ml-network: 3 | name: ml-network 4 | external: true 5 | services: 6 | ml: 7 | build: 8 | dockerfile: Dockerfile 9 | context: . 10 | args: 11 | PYTHON_VERSION: "3.10" 12 | tags: 13 | - {{cookiecutter.project_name}} 14 | ports: 15 | # exposes this port so if a fiftyone app is started, you can view the app in your browser at localhost:5151 16 | - 5151:5151 17 | shm_size: 64g 18 | volumes: 19 | - /fiftyone/data/media:/root/fiftyone 20 | - ~/{{cookiecutter.project_name}}:/home/{{cookiecutter.project_name}} 21 | command: tail -F anything 22 | networks: 23 | - ml-network 24 | privileged: true 25 | environment: 26 | - FIFTYONE_DATABASE_URI=mongodb://${MONGO_USER}:${MONGO_USER_PASSWORD}@${MONGO_IP}:27017/?authSource=admin 27 | - QDRANT_IP=${QDRANT_IP} 28 | - NVIDIA_DRIVER_CAPABILITIES=video,compute,utility 29 | deploy: 30 | resources: 31 | reservations: 32 | devices: 33 | - driver: nvidia 34 | # Only says one but it actually can use all gpus 35 | count: 1 36 | capabilities: [gpu] 37 | -------------------------------------------------------------------------------- /.github/workflows/pre-commit.yml: -------------------------------------------------------------------------------- 1 | name: pre-commit 2 | on: [push, pull_request] 3 | jobs: 4 | pre-commit: 5 | runs-on: ubuntu-latest 6 | name: ubuntu-latest 7 | steps: 8 | - uses: actions/checkout@v3 9 | - uses: actions/setup-python@v4 10 | with: 11 | python-version: "3.9" 12 | - run: | 13 | pip install --constraint=.github/workflows/constraints.txt pip 14 | pip install --constraint=.github/workflows/constraints.txt pre-commit 15 | - name: Compute cache key prefix 16 | if: matrix.os != 'windows-latest' 17 | id: cache_key_prefix 18 | shell: python 19 | run: | 20 | import hashlib 21 | import sys 22 | 23 | python = "py{}.{}".format(*sys.version_info[:2]) 24 | payload = sys.version.encode() + sys.executable.encode() 25 | digest = hashlib.sha256(payload).hexdigest() 26 | result = "${{ runner.os }}-{}-{}-pre-commit".format(python, digest) 27 | 28 | print("::set-output name=result::{}".format(result)) 29 | - uses: actions/cache@v3 30 | if: matrix.os != 'windows-latest' 31 | with: 32 | path: ~/.cache/pre-commit 33 | key: ${{ steps.cache_key_prefix.outputs.result }}-${{ hashFiles('.pre-commit-config.yaml') }} 34 | restore-keys: | 35 | ${{ steps.cache_key_prefix.outputs.result }}- 36 | - run: pre-commit run --all-files --show-diff-on-failure --color=always 37 | -------------------------------------------------------------------------------- /.github/workflows/update-instance.yml: -------------------------------------------------------------------------------- 1 | name: Update instance 2 | on: 3 | push: 4 | branches: 5 | - main 6 | concurrency: serialize 7 | env: 8 | TEMPLATE: cookiecutter-hypermodern-python 9 | PROJECT: cookiecutter-hypermodern-python-instance 10 | jobs: 11 | instance: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Check out ${{ env.TEMPLATE }} 15 | uses: actions/checkout@v3 16 | with: 17 | path: ${{ env.TEMPLATE }} 18 | - name: Check out ${{ env.PROJECT }} 19 | uses: actions/checkout@v3 20 | with: 21 | repository: "cjolowicz/${{ env.PROJECT }}" 22 | path: ${{ env.PROJECT }} 23 | token: ${{ secrets.X_GITHUB_TOKEN }} 24 | - name: Set up Python 3.10 25 | uses: actions/setup-python@v4 26 | with: 27 | python-version: "3.10" 28 | - name: Install cutty 29 | working-directory: ${{ env.TEMPLATE }} 30 | run: | 31 | pip install --constraint=.github/workflows/constraints.txt cutty 32 | cutty --version 33 | - name: Import commit into ${{ env.PROJECT }} 34 | run: | 35 | cutty import --non-interactive --cwd=${PROJECT} --revision=${GITHUB_SHA} 36 | env: 37 | GIT_AUTHOR_NAME: "GitHub Action" 38 | GIT_AUTHOR_EMAIL: "action@github.com" 39 | - name: Push to cjolowicz/${{ env.PROJECT }} 40 | run: | 41 | if ! git -C ${TEMPLATE} show --no-patch --format=%B ${GITHUB_SHA} | grep -q ^Retrocookie-Original-Commit: 42 | then 43 | git -C $PROJECT push https://$GITHUB_ACTOR:$GITHUB_TOKEN@github.com/cjolowicz/$PROJECT.git HEAD:main 44 | fi 45 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/.github/workflows/linting_and_type_checking.yaml: -------------------------------------------------------------------------------- 1 | name: Linting and Type Checking 2 | on: [push] 3 | jobs: 4 | black: 5 | runs-on: ubuntu-22.04 6 | steps: 7 | - name: Checkout branch 8 | uses: actions/checkout@v3.3.0 9 | - name: Setup Python 10 | uses: actions/setup-python@v4.5.0 11 | with: 12 | python-version: "3.10" 13 | - name: Install black 14 | run: pip install black 15 | - name: Check black 16 | run: black --verbose . 17 | 18 | flake8: 19 | runs-on: ubuntu-22.04 20 | steps: 21 | - name: Checkout branch 22 | uses: actions/checkout@v3.3.0 23 | - name: Setup Python 24 | uses: actions/setup-python@v4.5.0 25 | with: 26 | python-version: "3.10" 27 | - name: Install flake8 28 | run: pip install flake8 29 | - name: Check flake8 30 | run: flake8 --config dev_configurations/.flake8 . 31 | 32 | isort: 33 | runs-on: ubuntu-22.04 34 | steps: 35 | - name: Checkout branch 36 | uses: actions/checkout@v3.3.0 37 | - name: Setup Python 38 | uses: actions/setup-python@v4.5.0 39 | with: 40 | python-version: "3.10" 41 | - name: Install isort 42 | run: pip install isort 43 | - name: Check isort 44 | run: isort . --check --diff --verbose 45 | 46 | pyright: 47 | runs-on: ubuntu-22.04 48 | steps: 49 | - name: Checkout branch 50 | uses: actions/checkout@v3.3.0 51 | - name: Install poetry 52 | run: pip install poetry 53 | - name: Check poetry version 54 | run: poetry --version 55 | - name: Cache Python Dependencies 56 | uses: actions/setup-python@v4.5.0 57 | with: 58 | python-version: "3.10" 59 | cache: poetry 60 | - name: Install python packages for type checking 61 | run: poetry install 62 | - name: Check pyright 63 | run: poetry run pyright -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/dev_configurations/.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: local 3 | hooks: 4 | - id: black 5 | name: black 6 | entry: black 7 | language: system 8 | types: [python] 9 | require_serial: true 10 | - id: flake8 11 | name: flake8 12 | entry: flake8 13 | language: system 14 | types: [python] 15 | require_serial: true 16 | args: [--config, dev_configurations/.flake8] 17 | - id: isort 18 | name: isort 19 | entry: isort 20 | require_serial: true 21 | language: system 22 | types_or: [cython, pyi, python] 23 | args: ["--filter-files"] 24 | - repo: https://github.com/asottile/pyupgrade 25 | rev: v3.3.1 26 | hooks: 27 | - id: pyupgrade 28 | description: Automatically upgrade syntax for newer versions. 29 | args: [--py310-plus] 30 | - repo: https://github.com/pre-commit/pre-commit-hooks 31 | rev: v4.3.0 32 | hooks: 33 | - id: check-added-large-files 34 | description: Prevent giant files from being committed. 35 | args: ["--maxkb=500"] 36 | - id: check-ast 37 | description: Simply check whether files parse as valid python. 38 | - id: check-executables-have-shebangs 39 | description: Checks that non-binary executables have a proper shebang. 40 | - id: check-json 41 | description: Attempts to load all json files to verify syntax. 42 | - id: check-toml 43 | description: Attempts to load all TOML files to verify syntax. 44 | - id: check-yaml 45 | description: Attempts to load all yaml files to verify syntax. 46 | - id: end-of-file-fixer 47 | description: Makes sure files end in a newline and only a newline. 48 | - id: name-tests-test 49 | description: verifies that test files are named correctly. 50 | args: ["--pytest-test-first"] 51 | - id: trailing-whitespace 52 | description: Trims trailing whitespace. 53 | - repo: https://github.com/pre-commit/mirrors-prettier 54 | rev: v2.7.1 55 | hooks: 56 | - id: prettier 57 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM nvidia/cuda:12.1.0-cudnn8-devel-ubuntu22.04 2 | 3 | # change from /bin/sh to /bin/bash as the default shell 4 | SHELL ["/bin/bash", "-c"] 5 | 6 | RUN apt -y update; apt -y upgrade; \ 7 | apt -y --no-install-recommends install software-properties-common; \ 8 | add-apt-repository ppa:deadsnakes/ppa 9 | 10 | ARG PYTHON_VERSION 11 | 12 | # Install base ubuntu tools 13 | RUN apt -y update; apt -y upgrade; apt -y --no-install-recommends install \ 14 | screen \ 15 | htop \ 16 | wget \ 17 | curl \ 18 | git \ 19 | git-core \ 20 | zip \ 21 | unzip \ 22 | nano \ 23 | $(echo "python$PYTHON_VERSION") \ 24 | $(echo "python$PYTHON_VERSION-dev") \ 25 | $(echo "python$PYTHON_VERSION-distutils") \ 26 | python-is-python3 \ 27 | ; apt -y autoremove; apt -y clean 28 | 29 | RUN wget https://bootstrap.pypa.io/get-pip.py; \ 30 | $(echo "python$PYTHON_VERSION") get-pip.py; \ 31 | $(echo "python$PYTHON_VERSION") -m pip install --upgrade pip; \ 32 | rm get-pip.py 33 | 34 | # poetry-plugin-up lets you bump versions in pyproject.toml to the version you have installed if you have updated 35 | RUN pip install poetry; \ 36 | poetry config installer.max-workers 10; \ 37 | poetry self add poetry-plugin-up 38 | 39 | RUN mkdir -p /home/{{cookiecutter.project_name}} 40 | COPY poetry.lock pyproject.toml /home/{{cookiecutter.project_name}}/ 41 | WORKDIR /home/{{cookiecutter.project_name}}/ 42 | RUN poetry install; \ 43 | poetry run poe pytorch 44 | 45 | ENV PYTHONPATH="${PYTHONPATH}:/home/{{cookiecutter.project_name}}/src" 46 | 47 | # need git to install pre-commit hooks can be done after docker build but this is more fool-proof 48 | # If this is a docker image you will publish then this part should be removed. 49 | # This section is only so that the development experience is more natural: git commands from inside the container 50 | COPY .git .git 51 | RUN git config --global --add safe.directory /home/{{cookiecutter.project_name}} 52 | COPY dev_configurations/.pre-commit-config.yaml /home/{{cookiecutter.project_name}}/dev_configuration 53 | RUN poetry run pre-commit install -c /home/{{cookiecutter.project_name}}/dev_configurations/.pre-commit-config.yaml 54 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # Hypermodern Python Cookiecutter 2 | 3 | ```{toctree} 4 | --- 5 | hidden: true 6 | maxdepth: 1 7 | --- 8 | 9 | Quickstart 10 | guide 11 | contributing 12 | Code of Conduct 13 | license 14 | Changelog 15 | ``` 16 | 17 | ```{include} ../README.md 18 | --- 19 | start-after: 20 | end-before: 21 | --- 22 | ``` 23 | 24 | [Cookiecutter] template for a Python package 25 | based on the [Hypermodern Python] article series. 26 | 27 | ## Usage 28 | 29 | ```console 30 | $ cookiecutter gh:cjolowicz/cookiecutter-hypermodern-python \ 31 | --checkout="2022.6.3" 32 | ``` 33 | 34 | ## Features 35 | 36 | ```{include} ../README.md 37 | --- 38 | start-after: 39 | end-before: 40 | --- 41 | ``` 42 | 43 | ## FAQ 44 | 45 | ### What is this project about? 46 | 47 | The mission of this project is to 48 | enable current best practices 49 | through modern Python tooling. 50 | 51 | ### What makes this project different from other Python templates? 52 | 53 | This is a general-purpose template for Python libraries and applications. 54 | 55 | Our goals are: 56 | 57 | - Focus on simplicity and minimalism 58 | - Promote code quality through automation 59 | - Provide reliable and repeatable processes 60 | 61 | The project template is centered around the following tools: 62 | 63 | - [Poetry][1] for packaging and dependency management 64 | - [Nox][2] for automation of checks and other development tasks 65 | - [GitHub Actions][3] for continuous integration and delivery 66 | 67 | [1]: https://python-poetry.org/ 68 | [2]: https://nox.thea.codes/ 69 | [3]: https://github.com/features/actions 70 | 71 | ### Why is this Python template called "hypermodern"? 72 | 73 | [Hypermodernism] is a school of chess that dates back to more than a century ago. 74 | If this setup ever goes out of fashion, 75 | I can pretend it was my secret plan from the start. 76 | All images on the 77 | [associated blog][hypermodern python] show 78 | [past visions][retrofuturism] of the future. 79 | 80 | [cookiecutter]: https://github.com/audreyr/cookiecutter 81 | [hypermodern python]: https://medium.com/@cjolowicz/hypermodern-python-d44485d9d769 82 | [hypermodernism]: https://en.wikipedia.org/wiki/Hypermodernism_(chess) 83 | [retrofuturism]: https://en.wikipedia.org/wiki/Retrofuturism 84 | -------------------------------------------------------------------------------- /tools/dependencies-table.py: -------------------------------------------------------------------------------- 1 | import re 2 | from pathlib import Path 3 | 4 | import tomli 5 | 6 | 7 | PROJECT = Path("{{cookiecutter.project_name}}") 8 | JINJA_PATTERN = re.compile(r"{%.*%}") 9 | JINJA_PATTERN2 = re.compile(r"{{[^{]*}}") 10 | LINE_FORMAT = " {name:{width}} {description}" 11 | CANONICALIZE_PATTERN = re.compile(r"[-_.]+") 12 | DESCRIPTION_PATTERN = re.compile(r"\. .*") 13 | 14 | 15 | def canonicalize_name(name: str) -> str: 16 | # From ``packaging.utils.canonicalize_name`` (PEP 503) 17 | return CANONICALIZE_PATTERN.sub("-", name).lower() 18 | 19 | 20 | def truncate_description(description: str) -> str: 21 | """Truncate the description to the first sentence.""" 22 | return DESCRIPTION_PATTERN.sub(".", description) 23 | 24 | 25 | def format_dependency(dependency: str) -> str: 26 | """Format the dependency for the table.""" 27 | return "coverage__" if dependency == "coverage" else f"{dependency}_" 28 | 29 | 30 | def main() -> None: 31 | """Print restructuredText table of dependencies.""" 32 | path = PROJECT / "pyproject.toml" 33 | text = path.read_text() 34 | text = JINJA_PATTERN.sub("", text) 35 | text = JINJA_PATTERN2.sub("x", text) 36 | data = tomli.loads(text) 37 | 38 | dependencies = { 39 | canonicalize_name(dependency) 40 | for section in ["dependencies", "dev-dependencies"] 41 | for dependency in data["tool"]["poetry"][section].keys() 42 | if dependency != "python" 43 | } 44 | 45 | path = PROJECT / "poetry.lock" 46 | text = path.read_text() 47 | data = tomli.loads(text) 48 | 49 | descriptions = { 50 | canonicalize_name(package["name"]): truncate_description(package["description"]) 51 | for package in data["package"] 52 | if package["name"] in dependencies 53 | } 54 | 55 | table = { 56 | format_dependency(dependency): descriptions[dependency] 57 | for dependency in sorted(dependencies) 58 | } 59 | 60 | width = max(len(name) for name in table) 61 | width2 = max(len(description) for description in table.values()) 62 | separator = LINE_FORMAT.format( 63 | name="=" * width, width=width, description="=" * width2 64 | ) 65 | 66 | print(separator) 67 | 68 | for name, description in table.items(): 69 | line = LINE_FORMAT.format(name=name, width=width, description=description) 70 | 71 | print(line) 72 | 73 | print(separator) 74 | 75 | 76 | if __name__ == "__main__": 77 | main() 78 | -------------------------------------------------------------------------------- /.github/labels.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # Labels names are important as they are used by Release Drafter to decide 3 | # regarding where to record them in changelog or if to skip them. 4 | # 5 | # The repository labels will be automatically configured using this file and 6 | # the GitHub Action https://github.com/marketplace/actions/github-labeler. 7 | - name: breaking 8 | description: Breaking Changes 9 | color: 7c3ea5 10 | - name: bug 11 | description: Something isn't working 12 | color: d73a4a 13 | - name: build 14 | description: Changes that affect the build system or external dependencies 15 | color: fc964e 16 | - name: ci 17 | description: Changes to CI configuration files and scripts 18 | color: 00485e 19 | - name: cookiecutter 20 | description: Changes outside of the template directory 21 | color: f9ffb2 22 | - name: dependencies 23 | description: Dependencies 24 | color: 00ce44 25 | - name: documentation 26 | description: Improvements or additions to documentation 27 | color: 0075ca 28 | - name: duplicate 29 | description: This issue or pull request already exists 30 | color: cfd3d7 31 | - name: enhancement 32 | description: New feature or request 33 | color: a2eeef 34 | - name: github_actions 35 | description: Pull requests that update Github_actions code 36 | color: "000000" 37 | - name: good first issue 38 | description: Good for newcomers 39 | color: 7057ff 40 | - name: help wanted 41 | description: Extra attention is needed 42 | color: 008672 43 | - name: invalid 44 | description: This doesn't seem right 45 | color: e4e669 46 | - name: performance 47 | description: A code change that improves performance 48 | color: f3ffb2 49 | - name: python 50 | description: Pull requests that update Python code 51 | color: 2b67c6 52 | - name: question 53 | description: Further information is requested 54 | color: d876e3 55 | - name: refactoring 56 | description: A code change that neither fixes a bug nor adds a feature 57 | color: d4c5f9 58 | - name: removal 59 | description: Removals and Deprecations 60 | color: f00000 61 | - name: style 62 | description: 63 | Changes that do not affect the meaning of the code (white-space, formatting, 64 | etc) 65 | color: ffc6df 66 | - name: task 67 | description: 68 | color: d88c70 69 | - name: testing 70 | description: Adding missing tests or correcting existing tests 71 | color: 79e57f 72 | - name: wontfix 73 | description: This will not be worked on 74 | color: ffffff 75 | - name: "skip-changelog" 76 | description: Changes that should be omitted from the release notes 77 | color: ededed 78 | -------------------------------------------------------------------------------- /noxfile.py: -------------------------------------------------------------------------------- 1 | """Nox sessions.""" 2 | from pathlib import Path 3 | import shutil 4 | 5 | import nox 6 | from nox.sessions import Session 7 | 8 | nox.options.sessions = ["docs"] 9 | owner, repository = "ChickenTarm", "cookiecutter-python-ml-project" 10 | labels = "cookiecutter", "documentation" 11 | bump_paths = "README.md", "docs/guide.rst", "docs/index.rst", "docs/quickstart.md" 12 | 13 | 14 | @nox.session(name="prepare-release") 15 | def prepare_release(session: Session) -> None: 16 | """Prepare a GitHub release.""" 17 | args = [ 18 | f"--owner={owner}", 19 | f"--repository={repository}", 20 | *[f"--bump={path}" for path in bump_paths], 21 | *[f"--label={label}" for label in labels], 22 | *session.posargs, 23 | ] 24 | session.install("click", "github3.py") 25 | session.run("python", "tools/prepare-github-release.py", *args, external=True) 26 | 27 | 28 | @nox.session(name="publish-release") 29 | def publish_release(session: Session) -> None: 30 | """Publish a GitHub release.""" 31 | args = [f"--owner={owner}", f"--repository={repository}", *session.posargs] 32 | session.install("click", "github3.py") 33 | session.run("python", "tools/publish-github-release.py", *args, external=True) 34 | 35 | 36 | nox.options.sessions = ["linkcheck"] 37 | 38 | 39 | @nox.session 40 | def docs(session: Session) -> None: 41 | """Build the documentation.""" 42 | args = session.posargs or ["-W", "-n", "docs", "docs/_build"] 43 | 44 | if session.interactive and not session.posargs: 45 | args = ["-a", "--watch=docs/_static", "--open-browser", *args] 46 | 47 | builddir = Path("docs", "_build") 48 | if builddir.exists(): 49 | shutil.rmtree(builddir) 50 | 51 | session.install("-r", "docs/requirements.txt") 52 | 53 | if session.interactive: 54 | session.run("sphinx-autobuild", *args) 55 | else: 56 | session.run("sphinx-build", *args) 57 | 58 | 59 | @nox.session 60 | def linkcheck(session: Session) -> None: 61 | """Build the documentation.""" 62 | args = session.posargs or ["-b", "linkcheck", "-W", "--keep-going", "docs", "docs/_build"] 63 | 64 | builddir = Path("docs", "_build") 65 | if builddir.exists(): 66 | shutil.rmtree(builddir) 67 | 68 | session.install("-r", "docs/requirements.txt") 69 | 70 | session.run("sphinx-build", *args) 71 | 72 | 73 | @nox.session(name="dependencies-table") 74 | def dependencies_table(session: Session) -> None: 75 | """Print the dependencies table.""" 76 | session.install("tomli") 77 | session.run("python", "tools/dependencies-table.py", external=True) 78 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "{{cookiecutter.project_name}}" 3 | version = "{{cookiecutter.version}}" 4 | description = "{{cookiecutter.friendly_name}}" 5 | authors = ["{{cookiecutter.author}} <{{cookiecutter.email}}>"] 6 | readme = "README.md" 7 | homepage = "https://github.com/{{cookiecutter.github_user}}/{{cookiecutter.project_name}}" 8 | repository = "https://github.com/{{cookiecutter.github_user}}/{{cookiecutter.project_name}}" 9 | documentation = "https://{{cookiecutter.project_name}}.readthedocs.io" 10 | {% if cookiecutter.package_name != cookiecutter.project_name.replace('-', '_') -%} 11 | packages = [ 12 | { include = "{{cookiecutter.package_name}}", from = "src" }, 13 | ] 14 | {% endif -%} 15 | classifiers = [ 16 | "{{cookiecutter.development_status}}", 17 | ] 18 | 19 | [tool.poetry.urls] 20 | Changelog = "https://github.com/{{cookiecutter.github_user}}/{{cookiecutter.project_name}}/releases" 21 | 22 | [tool.poe.tasks] 23 | # This should only be called if you know you are using nvidia gpus 24 | pytorch = "python -m pip install torch==2.3.0+cu121 torchvision==0.18.0+cu121 -f https://download.pytorch.org/whl/torch_stable.html" 25 | 26 | [tool.poetry.dependencies] 27 | python = ">=3.10,<3.11" 28 | click = "^8.1.7" 29 | torch = "^2.3.1" 30 | torchvision = "^0.18.0" 31 | pytorch-lightning = "^2.3.3" 32 | wandb = "^0.17.4" 33 | fiftyone = "^0.24.1" 34 | numpy = "^2.0.0" 35 | opencv-python-headless = "^4.10.0.84" 36 | 37 | [tool.poetry.group.dev.dependencies] 38 | black = {extras = ["jupyter"], version = "^22.12.0"} 39 | flake8 = "^5.0.4" 40 | flake8-bandit = "^4.1.1" 41 | flake8-bugbear = "^22.12.6" 42 | flake8-docstrings = "^1.7.0" 43 | flake8-rst-docstrings = "^0.2.7" 44 | isort = "^5.13.2" 45 | pygments = "^2.18.0" 46 | pyright = "^1.1.370" 47 | pre-commit = "^2.21.0" 48 | nox = "^2022.11.21" 49 | nox-poetry = "^1.0.3" 50 | poethepoet = "^0.18.0" 51 | pep8-naming = "^0.13.3" 52 | 53 | [tool.poetry.group.test.dependencies] 54 | pytest = "^7.4.4" 55 | allure-pytest = "^2.13.5" 56 | coverage = {extras = ["toml"], version = "^6.5.0"} 57 | xdoctest = "^1.1.5" 58 | typeguard = "^2.13.3" 59 | safety = "^2.3.5" 60 | 61 | [tool.poetry.group.docs.dependencies] 62 | sphinx = "^5.3.0" 63 | sphinx-autobuild = "^2021.3.14" 64 | sphinx-click = "^4.4.0" 65 | furo = "^2022.12.7" 66 | 67 | [tool.poetry.scripts] 68 | {{cookiecutter.project_name}} = "{{cookiecutter.package_name}}.__main__:main" 69 | 70 | [tool.coverage.paths] 71 | source = ["src", "*/site-packages"] 72 | tests = ["tests", "*/tests"] 73 | 74 | [tool.coverage.run] 75 | branch = true 76 | source = ["{{cookiecutter.package_name}}", "tests"] 77 | 78 | [tool.coverage.report] 79 | show_missing = true 80 | fail_under = 100 81 | 82 | [tool.black] 83 | line-length = 120 84 | target-version = ["py310"] 85 | 86 | [tool.isort] 87 | profile = "black" 88 | line_length = 120 89 | include_trailing_comma=true 90 | 91 | [tool.pyright] 92 | include = ["."] 93 | 94 | typeCheckingMode = "basic" 95 | useLibraryCodeForTypes = true 96 | pythonPlatform = "Linux" 97 | pythonVersion = "3.10" 98 | verboseOutput = true 99 | 100 | reportMissingImports = true 101 | reportMissingTypeStubs = false 102 | reportGeneralTypeIssues = "warning" 103 | reportPrivateImportUsage = "none" 104 | reportUnboundVariable = "warning" 105 | 106 | [build-system] 107 | requires = ["poetry-core>=1.0.0"] 108 | build-backend = "poetry.core.masonry.api" 109 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | on: [push, pull_request] 3 | jobs: 4 | tests: 5 | strategy: 6 | fail-fast: false 7 | matrix: 8 | include: 9 | - { python-version: "3.10", os: ubuntu-latest } 10 | - { python-version: "3.10", os: windows-latest } 11 | - { python-version: "3.10", os: macos-latest } 12 | - { python-version: "3.9", os: ubuntu-latest } 13 | - { python-version: "3.8", os: ubuntu-latest } 14 | - { python-version: "3.7", os: ubuntu-latest } 15 | name: Python ${{ matrix.python-version }} (${{ matrix.os }}) 16 | runs-on: ${{ matrix.os }} 17 | steps: 18 | - uses: actions/checkout@v3 19 | with: 20 | path: cookiecutter-hypermodern-python 21 | - uses: actions/setup-python@v4 22 | with: 23 | python-version: ${{ matrix.python-version }} 24 | - name: Install tools using pip 25 | working-directory: cookiecutter-hypermodern-python 26 | run: | 27 | pip install --constraint=.github/workflows/constraints.txt pip 28 | pipx install --pip-args=--constraint=.github/workflows/constraints.txt cookiecutter 29 | pipx install --pip-args=--constraint=.github/workflows/constraints.txt nox 30 | pipx inject --pip-args=--constraint=.github/workflows/constraints.txt nox nox-poetry 31 | pipx install --pip-args=--constraint=.github/workflows/constraints.txt poetry 32 | - name: Generate project using Cookiecutter 33 | run: cookiecutter --no-input cookiecutter-hypermodern-python 34 | - name: Create git repository 35 | if: matrix.os != 'windows-latest' 36 | run: | 37 | git init 38 | git config --local user.name "GitHub Action" 39 | git config --local user.email "action@github.com" 40 | git add . 41 | git commit --message="Initial import" 42 | working-directory: hypermodern-python 43 | - name: Create git repository (Windows) 44 | if: matrix.os == 'windows-latest' 45 | run: | 46 | git init 47 | git config --local user.name "GitHub Action" 48 | git config --local user.email "action@github.com" 49 | # https://github.com/cookiecutter/cookiecutter/issues/405 50 | $ErrorActionPreference = "Continue" 51 | git add . 52 | $ErrorActionPreference = "Stop" 53 | git add --renormalize . 54 | git commit --message="Initial import" 55 | working-directory: hypermodern-python 56 | - name: Compute cache key for pre-commit 57 | if: matrix.os != 'windows-latest' 58 | id: cache_key 59 | shell: python 60 | run: | 61 | import hashlib 62 | import sys 63 | 64 | python = "py{}.{}".format(*sys.version_info[:2]) 65 | payload = sys.version.encode() + sys.executable.encode() 66 | digest = hashlib.sha256(payload).hexdigest() 67 | result = "${{ runner.os }}-{}-{}-pre-commit".format(python, digest) 68 | 69 | print("::set-output name=result::{}".format(result)) 70 | - uses: actions/cache@v3 71 | if: matrix.os != 'windows-latest' 72 | with: 73 | path: ~/.cache/pre-commit 74 | key: ${{ steps.cache_key.outputs.result }}-${{ hashFiles('hypermodern-python/.pre-commit-config.yaml') }} 75 | restore-keys: | 76 | ${{ steps.cache_key.outputs.result }}- 77 | - name: Run test suite using Nox 78 | run: nox --force-color 79 | working-directory: hypermodern-python 80 | - name: Install dependencies using Poetry 81 | run: poetry install --ansi 82 | working-directory: hypermodern-python 83 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/scripts/setup_host_env.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if ! command -v docker &> /dev/null 4 | then 5 | sudo apt-get update 6 | sudo apt-get install ca-certificates curl gnupg lsb-release 7 | sudo mkdir -p /etc/apt/keyrings 8 | curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg 9 | echo \ 10 | "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \ 11 | $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null 12 | sudo apt-get update 13 | sudo apt-get install docker-ce docker-ce-cli containerd.io docker-compose-plugin 14 | 15 | curl -s -L https://nvidia.github.io/nvidia-container-runtime/gpgkey | \ 16 | sudo apt-key add - 17 | distribution=$(. /etc/os-release;echo $ID$VERSION_ID) 18 | curl -s -L https://nvidia.github.io/nvidia-container-runtime/$distribution/nvidia-container-runtime.list | \ 19 | sudo tee /etc/apt/sources.list.d/nvidia-container-runtime.list 20 | sudo apt-get update 21 | sudo apt-get install -y nvidia-container-runtime 22 | sudo tee /etc/docker/daemon.json </dev/null 2>&1 || \ 59 | docker network create --driver bridge ml-network 60 | # Sets mongo to always be running this is done so that the database can be used across multiple projects 61 | sudo docker run -d --restart always --network ml-network -e MONGO_INITDB_ROOT_USERNAME=root \ 62 | -e MONGO_INITDB_ROOT_PASSWORD=password -v /fiftyone/data/db:/data/db \ 63 | -p 27017:27017 --name mongo-container mongo:latest 64 | # Should change these if security is a concern or exposed to the internet 65 | echo -e "MONGO_USER=\"root\"\nMONGO_USER_PASSWORD=\"password\""| sudo tee -a /etc/environment 66 | echo "MONGO_IP=\"$(sudo docker inspect -f '{{range.NetworkSettings.Networks}}{{.IPAddress}}{{end}}' mongo-container)\"" | sudo tee -a /etc/environment 67 | source /etc/environment 68 | fi 69 | 70 | if [ -d "/qdrant/storage" ] 71 | then 72 | echo "There is an existing qdrant db. If you want a fresh install please remove /qdrant/storage" 73 | else 74 | sudo mkdir -p /qdrant/storage 75 | # Checks if ml-network exists if not creates it 76 | docker network inspect ml-network >/dev/null 2>&1 || \ 77 | docker network create --driver bridge ml-network 78 | # Sets qdrant to always be running this is done so that the database can be used across multiple projects 79 | sudo docker run -d --restart always --network ml-network -v /qdrant/storage:/qdrant/storage \ 80 | -p 6333:6333 -p 6334:6334 --name qdrant-container qdrant/qdrant:latest 81 | echo "QDRANT_IP=\"$(sudo docker inspect -f '{{range.NetworkSettings.Networks}}{{.IPAddress}}{{end}}' qdrant-container)\"" | sudo tee -a /etc/environment 82 | source /etc/environment 83 | fi -------------------------------------------------------------------------------- /tools/publish-github-release.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import sys 3 | from typing import Optional 4 | 5 | import click 6 | import github3 7 | 8 | 9 | def publish_release(*, owner: str, repository_name: str, token: str, tag: str) -> None: 10 | github = github3.login(token=token) 11 | repository = github.repository(owner, repository_name) 12 | 13 | try: 14 | [pull_request] = list(repository.pull_requests(head=f"{owner}:release-{tag}")) 15 | except ValueError: 16 | raise RuntimeError( 17 | f"there should be exactly one pull request for {owner}:release-{tag}" 18 | ) 19 | 20 | pull_request = repository.pull_request(pull_request.number) 21 | 22 | try: 23 | [*_, commit] = pull_request.commits() 24 | except ValueError: 25 | raise RuntimeError( 26 | f"there should be at least one commit associated with #{pull_request.number}" 27 | ) 28 | 29 | try: 30 | [release] = [release for release in repository.releases() if release.draft] 31 | except ValueError: 32 | raise RuntimeError("there should be exactly one draft release") 33 | 34 | if commit.status().state != "success": 35 | raise RuntimeError(f"checks for #{pull_request.number} have failed") 36 | 37 | if pull_request.is_merged(): 38 | raise RuntimeError(f"#{pull_request.number} has been merged already") 39 | 40 | if not pull_request.mergeable: 41 | raise RuntimeError(f"#{pull_request.number} is not mergeable") 42 | 43 | title = f"{pull_request.title} (#{pull_request.number})" 44 | 45 | if not pull_request.merge(commit_title=title, merge_method="squash"): 46 | raise RuntimeError(f"cannot merge #{pull_request.number}") 47 | 48 | pull_request.refresh() 49 | 50 | if not pull_request.is_merged(): 51 | raise RuntimeError(f"#{pull_request.number} was not merged") 52 | 53 | click.echo(f"merged #{pull_request.number}") 54 | 55 | branch = repository.ref(f"heads/{pull_request.head.ref}") 56 | 57 | if not branch.delete(): 58 | raise RuntimeError(f"cannot remove {branch.ref}") 59 | 60 | click.echo(f"removed {branch.ref}") 61 | 62 | if not release.edit( 63 | tag_name=tag, 64 | name=tag, 65 | body=pull_request.body, 66 | draft=False, 67 | prerelease=False, 68 | ): 69 | raise RuntimeError(f"cannot publish {release.name}") 70 | 71 | click.echo(f"published {release.name}") 72 | 73 | 74 | @click.command() 75 | @click.option( 76 | "--owner", 77 | metavar="USER", 78 | required=True, 79 | envvar="GITHUB_USER", 80 | help="GitHub username", 81 | ) 82 | @click.option( 83 | "--repository", 84 | metavar="REPO", 85 | required=True, 86 | envvar="GITHUB_REPOSITORY", 87 | help="GitHub repository", 88 | ) 89 | @click.option( 90 | "--token", 91 | metavar="TOKEN", 92 | required=True, 93 | envvar="GITHUB_TOKEN", 94 | help="GitHub API token", 95 | ) 96 | @click.argument("tag", required=False) 97 | def main(owner: str, repository: str, token: str, tag: Optional[str]) -> None: 98 | """Publish a GitHub release for this project. 99 | 100 | If no release tag is specified, YYYY.MM.DD is used with the current date. 101 | There must be a single draft release, as well as a pull request for a 102 | branch `release-TAG`. This script merges the pull request and publishes 103 | the release, taking the release notes from the pull request description. 104 | """ 105 | if tag is None: 106 | today = datetime.date.today() 107 | tag = f"{today:%Y.%-m.%-d}" 108 | 109 | try: 110 | publish_release( 111 | owner=owner, 112 | repository_name=repository, 113 | token=token, 114 | tag=tag, 115 | ) 116 | except Exception as error: 117 | click.secho(f"error: {error}", fg="red") 118 | sys.exit(1) 119 | 120 | 121 | if __name__ == "__main__": 122 | main(prog_name="publish-github-release") 123 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/README.md: -------------------------------------------------------------------------------- 1 | # {{ cookiecutter.friendly_name }} 2 | 3 | [![Python Version](https://img.shields.io/badge/python-3.10-blue)][pypi status] 4 | [![pre-commit](https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit&logoColor=white)][pre-commit] 5 | [![poetry](https://img.shields.io/badge/package%20manager-poetry-blue)][poetry] 6 | 7 | [![sphinx](https://img.shields.io/badge/docs-sphinx-blue)][sphinx] 8 | [![Black](https://img.shields.io/badge/code%20style-black-000000.svg)][black] 9 | [![Pyright](https://img.shields.io/badge/type%20checker-pyright-blue.svg)][pyright] 10 | 11 | [pypi status]: https://pypi.org/project/{{cookiecutter.project_name}}/ 12 | [poetry]: https://github.com/python-poetry/poetry 13 | [pre-commit]: https://github.com/pre-commit/pre-commit 14 | [sphinx]: https://github.com/sphinx-doc/sphinx 15 | [black]: https://github.com/psf/black 16 | [pyright]: https://github.com/microsoft/pyright 17 | 18 | ## Requirement 19 | 20 | This has only been tested on Ubuntu 22.04 21 | 22 | 23 | ## Installation 24 | 25 | ``` 26 | ./scripts/setup_host.env 27 | docker compose --env-file /etc/environment up -d --build --force-recreate 28 | ``` 29 | 30 | ``` 31 | ./scripts/setup_host.env 32 | ``` 33 | This will check if docker is installed, if not then it will install docker along with the compose plugin for docker and the container runtime for nvidia hardware. It will also set the default configuration of docker to use nvidia as the default runtime. This will enable access to nvidia hardware during the build process of a Dockerfile and not just on run. 34 | 35 | The script will also check for and automatically create a docker network for mongodb so that a future cookiecutter project using this template is able to connect to and utilize mongodb which is storing the data for fiftyone. This will help centralize the source of truth for data and reduce multiple copies of large datasets like COCO. Then the monogdb container is started up on that network and the IP of the database is fetched and stored in /etc/environment so all applications will be able to find it. The data is stored at /fiftyone/data/db to make backups easier. When doing an upgrade, it makes for easier backing up and exporting of the data. 36 | 37 | ``` 38 | docker compose --env-file /etc/environment up -d --build 39 | ``` 40 | This will build your development environment. It copies in the poetry.lock and pyproject.toml which will check for changes in the environment. It will build and install all the packages you have defined. 41 | 42 | After the image is built the container is attached to the mongo docker network, and the environment variables that fiftyone needs to access mongo are passed in. 43 | 44 | The default command for the container allows it to run indefinitely. This is to allow for a long running container so VSCode can attach to it along with the useful debugging tools. VSCode is not the only IDE that can connect to a docker container, but it is the best IDE that I have seen in doing that. 45 | 46 | 47 | ## Tests 48 | 49 | To run all tests: 50 | 51 | ` 52 | nox --session tests 53 | ` 54 | 55 | If you want to run a specific file to debug it: 56 | 57 | ` 58 | pytest 59 | ` 60 | 61 | ## Pre-commit 62 | 63 | Install: 64 | 65 | ` 66 | pre-commit install dev_configurations/.pre-commit-config.yaml 67 | ` 68 | 69 | Running it on existing files 70 | ` 71 | nox --session pre-commit 72 | ` 73 | 74 | ### Adding new pre-commit hooks 75 | pre-commit has the ability to use local installation of programs for the hooks, but the git repo along with the release or commit version is the better method. 76 | 77 | 78 | ## Documentation 79 | 80 | To build documentation for the code: 81 | ` 82 | nox --session docs-build 83 | ` 84 | 85 | ## CI 86 | The CI is located at .github 87 | 88 | The tests in the ci will run and produce an artifact that will be used for test report via allure. 89 | 90 | The results should be viewable throught Settings -> Pages 91 | 92 | ## Poetry 93 | ### Adding a package 94 | The poetry equivalent to pip is underneath. 95 | Pip: 96 | 97 | ` 98 | pip install 99 | ` 100 | 101 | Poetry: 102 | 103 | ` 104 | poetry add 105 | ` 106 | 107 | To add a package that is only used for development purposes: 108 | 109 | ` 110 | poetry add --group=dev 111 | ` 112 | ### Remove a package 113 | Pip: 114 | 115 | ` 116 | pip uninstall 117 | ` 118 | 119 | Poetry: 120 | 121 | ` 122 | poetry remove 123 | ` 124 | 125 | To remove a package that is only used for development purposes: 126 | 127 | ` 128 | poetry remove --group=dev 129 | ` 130 | ### Installing dependencies from a file 131 | Pip: 132 | 133 | ` 134 | pip install -r requirements.txt 135 | ` 136 | 137 | Poetry 138 | 139 | ` 140 | poetry install 141 | ` 142 | 143 | To install non-development dependencies 144 | 145 | ` 146 | poetry install --without=dev 147 | ` 148 | -------------------------------------------------------------------------------- /docs/quickstart.md: -------------------------------------------------------------------------------- 1 | # Quickstart Guide 2 | 3 | ## Requirements 4 | 5 | Install [Cookiecutter]: 6 | 7 | ```console 8 | $ pipx install cookiecutter 9 | ``` 10 | 11 | Install [Poetry] by downloading and running [install-poetry.py]: 12 | 13 | ```console 14 | $ python install-poetry.py 15 | ``` 16 | 17 | Install [Nox] and [nox-poetry]: 18 | 19 | ```console 20 | $ pipx install nox 21 | $ pipx inject nox nox-poetry 22 | ``` 23 | 24 | [pipx] is preferred, but you can also install with `pip install --user`. 25 | 26 | It is recommended to set up Python 3.7, 3.8, 3.9, 3.10 using [pyenv]. 27 | 28 | ## Creating a project 29 | 30 | Generate a Python project: 31 | 32 | ```console 33 | $ cookiecutter gh:cjolowicz/cookiecutter-python-ml-project \ 34 | --checkout="2022.6.3" 35 | ``` 36 | 37 | Change to the root directory of your new project, 38 | and create a Git repository: 39 | 40 | ```console 41 | $ git init 42 | $ git add . 43 | $ git commit 44 | ``` 45 | 46 | ## Running 47 | 48 | Run the command-line interface from the source tree: 49 | 50 | ```console 51 | $ poetry install 52 | $ poetry run 53 | ``` 54 | 55 | Run an interactive Python session: 56 | 57 | ```console 58 | $ poetry install 59 | $ poetry run python 60 | ``` 61 | 62 | ## Testing 63 | 64 | Run the full test suite: 65 | 66 | ```console 67 | $ nox 68 | ``` 69 | 70 | List the available Nox sessions: 71 | 72 | ```console 73 | $ nox --list-sessions 74 | ``` 75 | 76 | Install the pre-commit hooks: 77 | 78 | ```console 79 | $ nox -s pre-commit -- install 80 | ``` 81 | 82 | ## Continuous Integration 83 | 84 | ### GitHub 85 | 86 | 1. Sign up at [GitHub]. 87 | 2. Create an empty repository for your project. 88 | 3. Follow the instructions to push an existing repository from the command line. 89 | 90 | ### PyPI 91 | 92 | 1. Sign up at [PyPI]. 93 | 2. Go to the Account Settings on PyPI, 94 | generate an API token, and copy it. 95 | 3. Go to the repository settings on GitHub, and 96 | add a secret named `PYPI_TOKEN` with the token you just copied. 97 | 98 | ### TestPyPI 99 | 100 | 1. Sign up at [TestPyPI]. 101 | 2. Go to the Account Settings on TestPyPI, 102 | generate an API token, and copy it. 103 | 3. Go to the repository settings on GitHub, and 104 | add a secret named `TEST_PYPI_TOKEN` with the token you just copied. 105 | 106 | ### Codecov 107 | 108 | 1. Sign up at [Codecov]. 109 | 2. Install their GitHub app. 110 | 111 | ### Read the Docs 112 | 113 | 1. Sign up at [Read the Docs]. 114 | 2. Import your GitHub repository, using the button _Import a Project_. 115 | 3. Install the GitHub webhook, 116 | using the button _Add integration_ 117 | on the _Integrations_ tab 118 | in the _Admin_ section of your project 119 | on Read the Docs. 120 | 121 | ## Releasing 122 | 123 | Releases are triggered by a version bump on the default branch. 124 | It is recommended to do this in a separate pull request: 125 | 126 | 1. Switch to a branch. 127 | 2. Bump the version using [poetry version]. 128 | 3. Commit and push to GitHub. 129 | 4. Open a pull request. 130 | 5. Merge the pull request. 131 | 132 | The Release workflow performs the following automated steps: 133 | 134 | - Build and upload the package to PyPI. 135 | - Apply a version tag to the repository. 136 | - Publish a GitHub Release. 137 | 138 | Release notes are populated with the titles and authors of merged pull requests. 139 | You can group the pull requests into separate sections 140 | by applying labels to them, like this: 141 | 142 | 143 | 144 | | Pull Request Label | Section in Release Notes | 145 | | ------------------ | ---------------------------- | 146 | | `breaking` | 💥 Breaking Changes | 147 | | `enhancement` | 🚀 Features | 148 | | `removal` | 🔥 Removals and Deprecations | 149 | | `bug` | 🐞 Fixes | 150 | | `performance` | 🐎 Performance | 151 | | `testing` | 🚨 Testing | 152 | | `ci` | 👷 Continuous Integration | 153 | | `documentation` | 📚 Documentation | 154 | | `refactoring` | 🔨 Refactoring | 155 | | `style` | 💄 Style | 156 | | `dependencies` | 📦 Dependencies | 157 | 158 | 159 | 160 | [codecov]: https://codecov.io/ 161 | [cookiecutter]: https://github.com/audreyr/cookiecutter 162 | [github]: https://github.com/ 163 | [install-poetry.py]: https://raw.githubusercontent.com/python-poetry/poetry/master/install-poetry.py 164 | [nox]: https://nox.thea.codes/ 165 | [nox-poetry]: https://nox-poetry.readthedocs.io/ 166 | [pipx]: https://pipxproject.github.io/pipx/ 167 | [poetry]: https://python-poetry.org/ 168 | [poetry version]: https://python-poetry.org/docs/cli/#version 169 | [pyenv]: https://github.com/pyenv/pyenv 170 | [pypi]: https://pypi.org/ 171 | [read the docs]: https://readthedocs.org/ 172 | [testpypi]: https://test.pypi.org/ 173 | -------------------------------------------------------------------------------- /tools/prepare-github-release.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import subprocess 3 | import sys 4 | from pathlib import Path 5 | from typing import Any 6 | from typing import Iterable 7 | from typing import List 8 | from typing import Optional 9 | 10 | import click 11 | import github3 12 | 13 | 14 | def git(*args: str, **kwargs: Any) -> str: 15 | try: 16 | process = subprocess.run( 17 | ["git", *args], check=True, capture_output=True, text=True 18 | ) 19 | return process.stdout 20 | except subprocess.CalledProcessError as error: 21 | print(error.stdout, end="") 22 | print(error.stderr, end="", file=sys.stderr) 23 | raise 24 | 25 | 26 | def replace_text(path: Path, old: str, new: str): 27 | text = path.read_text() 28 | text = text.replace(old, new) 29 | path.write_text(text) 30 | 31 | 32 | def prepare_release( 33 | *, 34 | owner: str, 35 | repository_name: str, 36 | token: str, 37 | tag: str, 38 | remote: str, 39 | base: str, 40 | bump_paths: List[Path], 41 | label_names: List[str], 42 | ) -> None: 43 | branch = f"release-{tag}" 44 | title = f"Release {tag}" 45 | oldtag = git("describe", "--tags", "--abbrev=0").strip() 46 | 47 | git("switch", f"--create={branch}", base) 48 | 49 | for path in bump_paths: 50 | replace_text(path, oldtag, tag) 51 | git("add", str(path)) 52 | 53 | git("commit", f"--message={title}") 54 | git("push", "--set-upstream", remote, branch) 55 | 56 | click.echo(f"pushed {branch}") 57 | 58 | github = github3.login(token=token) 59 | repository = github.repository(owner, repository_name) 60 | 61 | try: 62 | [release] = [release for release in repository.releases() if release.draft] 63 | except ValueError: 64 | raise RuntimeError("there should be exactly one draft release") 65 | 66 | pull_request = repository.create_pull( 67 | title=title, 68 | base=base, 69 | head=f"{owner}:{branch}", 70 | body=release.body, 71 | ) 72 | 73 | click.echo(f"opened #{pull_request.number}") 74 | 75 | pull_request = repository.pull_request(pull_request.number) 76 | labels = pull_request.issue().add_labels(*label_names) 77 | 78 | for name in label_names: 79 | if name not in {label.name for label in labels}: 80 | raise RuntimeError(f"label {name} missing from #{pull_request.number}") 81 | 82 | click.echo(f"added labels {', '.join(label_names)} to #{pull_request.number}") 83 | 84 | 85 | @click.command() 86 | @click.option( 87 | "--owner", 88 | metavar="USER", 89 | required=True, 90 | envvar="GITHUB_USER", 91 | help="GitHub username", 92 | ) 93 | @click.option( 94 | "--repository", 95 | metavar="REPO", 96 | required=True, 97 | envvar="GITHUB_REPOSITORY", 98 | help="GitHub repository", 99 | ) 100 | @click.option( 101 | "--token", 102 | metavar="TOKEN", 103 | required=True, 104 | envvar="GITHUB_TOKEN", 105 | help="GitHub API token", 106 | ) 107 | @click.option( 108 | "--remote", 109 | metavar="REMOTE", 110 | default="origin", 111 | help="remote for GitHub repository", 112 | ) 113 | @click.option( 114 | "--base", 115 | metavar="BRANCH", 116 | default="main", 117 | help="default branch of the GitHub repository", 118 | ) 119 | @click.option( 120 | "--bump", 121 | metavar="FILE", 122 | multiple=True, 123 | help="bump the version in these files (may be specified multiple times)", 124 | ) 125 | @click.option( 126 | "labels", 127 | "--label", 128 | metavar="LABEL", 129 | multiple=True, 130 | help="labels for the pull request (may be specified multiple times)", 131 | ) 132 | @click.argument("tag", required=False) 133 | def main( 134 | owner: str, 135 | repository: str, 136 | token: str, 137 | remote: str, 138 | base: str, 139 | bump: Iterable[str], 140 | labels: Iterable[str], 141 | tag: Optional[str], 142 | ) -> None: 143 | """Open a pull request to release this project. 144 | 145 | If no release tag is specified, YYYY.MM.DD is used with the current date. 146 | There must be a single draft release. This script pushes a branch 147 | `release-TAG` and opens a pull request for it taking the pull request 148 | description from the draft release notes. The branch contains a single 149 | commit which updates the version number in the documentation. 150 | """ 151 | 152 | if tag is None: 153 | today = datetime.date.today() 154 | tag = f"{today:%Y.%-m.%-d}" 155 | 156 | try: 157 | prepare_release( 158 | owner=owner, 159 | repository_name=repository, 160 | token=token, 161 | remote=remote, 162 | base=base, 163 | bump_paths=[Path(path) for path in bump], 164 | label_names=list(labels), 165 | tag=tag, 166 | ) 167 | except Exception as error: 168 | click.secho(f"error: {error}", fg="red") 169 | sys.exit(1) 170 | 171 | 172 | if __name__ == "__main__": 173 | main(prog_name="prepare-github-release") 174 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributor Guide 2 | 3 | Thank you for your interest in improving the Hypermodern Python Cookiecutter. 4 | This project is open-source under the [MIT license] and 5 | welcomes contributions in the form of bug reports, feature requests, and pull requests. 6 | 7 | Here is a list of important resources for contributors: 8 | 9 | - [Source Code] 10 | - [Documentation] 11 | - [Issue Tracker] 12 | - [Code of Conduct] 13 | 14 | ## How to report a bug 15 | 16 | Report bugs on the [Issue Tracker]. 17 | 18 | When filing an issue, make sure to answer these questions: 19 | 20 | - Which operating system and Python version are you using? 21 | - Which version of this project are you using? 22 | - What did you do? 23 | - What did you expect to see? 24 | - What did you see instead? 25 | 26 | The best way to get your bug fixed is to provide a test case, 27 | and/or steps to reproduce the issue. 28 | 29 | ## How to request a feature 30 | 31 | Request features on the [Issue Tracker]. 32 | 33 | ## How to set up your development environment 34 | 35 | You need Python 3.7+ and the following tools: 36 | 37 | - [Cookiecutter] 38 | - [Poetry] 39 | - [Nox] 40 | - [nox-poetry] 41 | 42 | Fork the repository on [GitHub], 43 | and clone the fork to your local machine. You can now generate a project 44 | from your development version: 45 | 46 | ```console 47 | $ cookiecutter path/to/cookiecutter-hypermodern-python 48 | ``` 49 | 50 | You may also want to push your generated project to GitHub, 51 | and set up [continuous integration]. 52 | 53 | ## How to test the project 54 | 55 | Please refer to the [User Guide] 56 | for instructions on how to run the test suite locally. 57 | 58 | ## How to submit changes 59 | 60 | Open a [pull request] to submit changes to this project. 61 | 62 | Your pull request needs to meet the following guidelines for acceptance: 63 | 64 | - The Nox test suite must pass without errors and warnings. 65 | - Include unit tests. This project maintains 100% code coverage. 66 | - If your changes add functionality, update the documentation accordingly. 67 | 68 | Feel free to submit early, though—we can always iterate on this. 69 | 70 | It is recommended to open an issue before starting work on anything. 71 | This will allow a chance to talk it over with the owners and validate your approach. 72 | 73 | ## How to accept changes 74 | 75 | _You need to be a project maintainer to accept changes._ 76 | 77 | Before accepting a pull request, go through the following checklist: 78 | 79 | - The PR must pass all checks. 80 | - The PR must have a descriptive title. 81 | - The PR should be labelled with the kind of change (see below). 82 | 83 | Release notes are pre-filled with titles and authors of merged pull requests. 84 | Labels group the pull requests into sections. 85 | The following list shows the available sections, 86 | with associated labels in parentheses: 87 | 88 | - 💥 Breaking Changes (`breaking`) 89 | - 🚀 Features (`enhancement`) 90 | - 🔥 Removals and Deprecations (`removal`) 91 | - 🐞 Fixes (`bug`) 92 | - 🐎 Performance (`performance`) 93 | - 🚨 Testing (`testing`) 94 | - 👷 Continuous Integration (`ci`) 95 | - 📚 Documentation (`documentation`) 96 | - 🔨 Refactoring (`refactoring`) 97 | - 💄 Style (`style`) 98 | - 📦 Dependencies (`dependencies`) 99 | 100 | To merge the pull request, follow these steps: 101 | 102 | 1. Click **Squash and Merge**. 103 | (Select this option from the dropdown menu of the merge button, if it is not shown.) 104 | 2. Click **Confirm squash and merge**. 105 | 3. Click **Delete branch**. 106 | 107 | ## How to make a release 108 | 109 | _You need to be a project maintainer to make a release._ 110 | 111 | Before making a release, go through the following checklist: 112 | 113 | - All pull requests for the release have been merged. 114 | - The default branch passes all checks. 115 | 116 | Releases are made by publishing a GitHub Release. 117 | A draft release is being maintained based on merged pull requests. 118 | To publish the release, follow these steps: 119 | 120 | 1. Click **Edit** next to the draft release. 121 | 2. Enter a tag with the new version. 122 | 3. Enter the release title, also the new version. 123 | 4. Edit the release description, if required. 124 | 5. Click **Publish Release**. 125 | 126 | Version numbers adhere to [Calendar Versioning], 127 | of the form `YYYY.MM.DD`. 128 | 129 | After publishing the release, the following automated steps are triggered: 130 | 131 | - The Git tag is applied to the repository. 132 | - [Read the Docs] builds a new stable version of the documentation. 133 | 134 | [calendar versioning]: https://calver.org/ 135 | [continuous integration]: https://cookiecutter-hypermodern-python.readthedocs.io/en/stable/quickstart.html#continuous-integration 136 | [cookiecutter]: https://cookiecutter.readthedocs.io/ 137 | [documentation]: https://cookiecutter-hypermodern-python.readthedocs.io/ 138 | [github]: https://github.com/cjolowicz/cookiecutter-hypermodern-python 139 | [issue tracker]: https://github.com/cjolowicz/cookiecutter-hypermodern-python/issues 140 | [mit license]: https://opensource.org/licenses/MIT 141 | [nox]: https://nox.thea.codes/ 142 | [nox-poetry]: https://nox-poetry.readthedocs.io/ 143 | [poetry]: https://python-poetry.org/ 144 | [pull request]: https://github.com/cjolowicz/cookiecutter-hypermodern-python/pulls 145 | [read the docs]: https://cookiecutter-hypermodern-python.readthedocs.io/ 146 | [source code]: https://github.com/cjolowicz/cookiecutter-hypermodern-python 147 | [user guide]: https://cookiecutter-hypermodern-python.readthedocs.io/en/latest/guide.html#how-to-test-your-project 148 | 149 | 150 | 151 | [code of conduct]: CODE_OF_CONDUCT.md 152 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # cookiecutter-python-ml-project 2 | 3 | 4 | 5 | [![License][license badge]][license]
6 | [![pre-commit enabled][pre-commit badge]][pre-commit project] 7 | [![Black codestyle][black badge]][black project] 8 | [![Contributor Covenant][contributor covenant badge]][code of conduct] 9 | 10 | [black badge]: https://img.shields.io/badge/code%20style-black-000000.svg 11 | [black project]: https://github.com/psf/black 12 | [calver badge]: https://img.shields.io/badge/calver-YYYY.MM.DD-22bfda.svg 13 | [code of conduct]: https://github.com/ChickenTarm/my-ml-python-cookiecutter/blob/main/CODE_OF_CONDUCT.md 14 | [contributor covenant badge]: https://img.shields.io/badge/Contributor%20Covenant-2.1-4baaaa.svg 15 | [github actions badge]: https://github.com/ChickenTarm/my-ml-python-cookiecutter/workflows/Tests/badge.svg 16 | [github page]: https://github.com/ChickenTarm/my-ml-python-cookiecutter 17 | [license badge]: https://img.shields.io/github/license/ChickenTarm/cookiecutter-python-ml-project 18 | [license]: https://opensource.org/licenses/MIT 19 | [pre-commit badge]: https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit&logoColor=white 20 | [pre-commit project]: https://pre-commit.com/ 21 | 22 | 23 | 24 |

25 | 26 |

27 | 28 | A [Cookiecutter] template for ML Python package based on the [Automatic ML Project Setup] article. 29 | 30 | This is designed with reproducibility, distribution, and easy data wrangling and exploration in mind. Many different ml repos have differing: data structures, training loops, visualizations, and deployment. 31 | 32 | - [fiftyone] is used to ensure standardization of formats. Many different datasets for the same task have different formats. Fiftyone fixes this since it has built many integrations for importing and exporting data. This makes data loading, and visualization much easier. 33 | - [lightning] is used to put structure to machine learning code. It has a standard control flow where it is easy to learn. Lightning also has many integrations and abstractions that make training much more efficient and scalable. 34 | - [wandb] is used to help visualize the actual training process. It allows for powerful and custom visualization needs and experiment comparison. 35 | - Lastly, environment management is one of the biggest issues with ml. Too many machine learning repos do not have a [docker] container which makes cloning and using the project more difficult. Everything should be ran in containers unless there is a specific reason not to. 36 | 37 | 38 | [cookiecutter]: https://github.com/audreyr/cookiecutter 39 | [automatic ml project setup]: https://medium.com/voxel51/automatically-set-up-a-new-ml-project-pain-free-voxel51-1b900daaaf77 40 | [hypermodern python cookiecutter]: https://github.com/cjolowicz/cookiecutter-hypermodern-python 41 | 42 | ## Usage 43 | 44 | ```console 45 | $ cookiecutter https://github.com/ChickenTarm/cookiecutter-python-ml-project.git 46 | ``` 47 | 48 | ## Features 49 | 50 | 51 | 52 | - Containerization and templated deployment services with [Docker] 53 | - Data management and visualization with [fiftyone] and [mongodb] 54 | - Packaging and dependency management with [Poetry] 55 | - Test automation with [Nox] 56 | - Linting with [pre-commit] and [Flake8] 57 | - Continuous integration with [GitHub Actions] 58 | - Documentation with [Sphinx], [MyST], and [Read the Docs] using the [furo] theme 59 | - Automated uploads to [PyPI] and [TestPyPI] 60 | - Automated dependency updates with [Dependabot] 61 | - Code formatting with [Black] and [Prettier] 62 | - Import sorting with [isort] 63 | - Testing with [pytest] 64 | - Code coverage with [Coverage.py] 65 | - Coverage reporting with [Codecov] 66 | - Command-line interface with [Click] 67 | - Static type-checking with [mypy] 68 | - Runtime type-checking with [Typeguard] 69 | - Automated Python syntax upgrades with [pyupgrade] 70 | - Security audit with [Bandit] and [Safety] 71 | - Check documentation examples with [xdoctest] 72 | - Generate API documentation with [autodoc] and [napoleon] 73 | - Generate command-line reference with [sphinx-click] 74 | 75 | The template supports Python 3.7, 3.8, 3.9, and 3.10. 76 | 77 | 78 | [autodoc]: https://www.sphinx-doc.org/en/master/usage/extensions/autodoc.html 79 | [bandit]: https://github.com/PyCQA/bandit 80 | [black]: https://github.com/psf/black 81 | [click]: https://click.palletsprojects.com/ 82 | [codecov]: https://codecov.io/ 83 | [coverage.py]: https://coverage.readthedocs.io/ 84 | [dependabot]: https://dependabot.com/ 85 | [docker]: https://www.docker.com 86 | [fiftyone]: https://github.com/voxel51/fiftyone 87 | [flake8]: http://flake8.pycqa.org 88 | [furo]: https://pradyunsg.me/furo/ 89 | [github actions]: https://github.com/features/actions 90 | [github labeler]: https://github.com/marketplace/actions/github-labeler 91 | [isort]: https://pycqa.github.io/isort/ 92 | [lightning]: https://www.pytorchlightning.ai 93 | [mongodb]: https://github.com/mongodb/mongo 94 | [mypy]: http://mypy-lang.org/ 95 | [myst]: https://myst-parser.readthedocs.io/ 96 | [napoleon]: https://www.sphinx-doc.org/en/master/usage/extensions/napoleon.html 97 | [nox]: https://nox.thea.codes/ 98 | [poetry]: https://python-poetry.org/ 99 | [pre-commit]: https://pre-commit.com/ 100 | [prettier]: https://prettier.io/ 101 | [pypi]: https://pypi.org/ 102 | [pytest]: https://docs.pytest.org/en/latest/ 103 | [pyupgrade]: https://github.com/asottile/pyupgrade 104 | [read the docs]: https://readthedocs.org/ 105 | [release drafter]: https://github.com/release-drafter/release-drafter 106 | [safety]: https://github.com/pyupio/safety 107 | [sphinx]: http://www.sphinx-doc.org/ 108 | [sphinx-click]: https://sphinx-click.readthedocs.io/ 109 | [testpypi]: https://test.pypi.org/ 110 | [typeguard]: https://github.com/agronholm/typeguard 111 | [wandb]: https://wandb.ai/site 112 | [xdoctest]: https://github.com/Erotemic/xdoctest 113 | 114 | 115 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, caste, color, religion, or sexual 10 | identity and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | - Demonstrating empathy and kindness toward other people 21 | - Being respectful of differing opinions, viewpoints, and experiences 22 | - Giving and gracefully accepting constructive feedback 23 | - Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | - Focusing on what is best not just for us as individuals, but for the overall 26 | community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | - The use of sexualized language or imagery, and sexual attention or advances of 31 | any kind 32 | - Trolling, insulting or derogatory comments, and personal or political attacks 33 | - Public or private harassment 34 | - Publishing others' private information, such as a physical or email address, 35 | without their explicit permission 36 | - Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | [mail@claudiojolowicz.com](mailto:mail@claudiojolowicz.com). 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series of 86 | actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or permanent 93 | ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within the 113 | community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.1, available at 119 | [https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. 120 | 121 | Community Impact Guidelines were inspired by 122 | [Mozilla's code of conduct enforcement ladder][mozilla coc]. 123 | 124 | For answers to common questions about this code of conduct, see the FAQ at 125 | [https://www.contributor-covenant.org/faq][faq]. Translations are available at 126 | [https://www.contributor-covenant.org/translations][translations]. 127 | 128 | [homepage]: https://www.contributor-covenant.org 129 | [v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html 130 | [mozilla coc]: https://github.com/mozilla/diversity 131 | [faq]: https://www.contributor-covenant.org/faq 132 | [translations]: https://www.contributor-covenant.org/translations 133 | -------------------------------------------------------------------------------- /{{cookiecutter.project_name}}/noxfile.py: -------------------------------------------------------------------------------- 1 | """Nox sessions.""" 2 | import os 3 | import shlex 4 | import shutil 5 | import sys 6 | from pathlib import Path 7 | from textwrap import dedent 8 | 9 | import nox 10 | 11 | try: 12 | from nox_poetry import Session, session 13 | except ImportError: 14 | message = f"""\ 15 | Nox failed to import the 'nox-poetry' package. 16 | 17 | Please install it using the following command: 18 | 19 | {sys.executable} -m pip install nox-poetry""" 20 | raise SystemExit(dedent(message)) from None 21 | 22 | 23 | package = "{{cookiecutter.package_name}}" 24 | python_versions = ["3.10"] 25 | nox.needs_version = ">= 2022.11.21" 26 | nox.options.sessions = ( 27 | "pre-commit", 28 | "safety", 29 | "pyright", 30 | "tests", 31 | "typeguard", 32 | "xdoctest", 33 | "docs-build", 34 | ) 35 | 36 | 37 | def activate_virtualenv_in_precommit_hooks(session: Session) -> None: 38 | """Activate virtualenv in hooks installed by pre-commit. 39 | 40 | This function patches git hooks installed by pre-commit to activate the 41 | session's virtual environment. This allows pre-commit to locate hooks in 42 | that environment when invoked from git. 43 | 44 | Parameters 45 | ---------- 46 | session : Session 47 | The Session object. 48 | """ 49 | assert session.bin is not None # noqa: S101 50 | 51 | # Only patch hooks containing a reference to this session's bindir. Support 52 | # quoting rules for Python and bash, but strip the outermost quotes so we 53 | # can detect paths within the bindir, like /python. 54 | bindirs = [ 55 | bindir[1:-1] if bindir[0] in "'\"" else bindir for bindir in (repr(session.bin), shlex.quote(session.bin)) 56 | ] 57 | 58 | virtualenv = session.env.get("VIRTUAL_ENV") 59 | if virtualenv is None: 60 | return 61 | 62 | headers = { 63 | # pre-commit < 2.16.0 64 | "python": f"""\ 65 | import os 66 | os.environ["VIRTUAL_ENV"] = {virtualenv!r} 67 | os.environ["PATH"] = os.pathsep.join(( 68 | {session.bin!r}, 69 | os.environ.get("PATH", ""), 70 | )) 71 | """, 72 | # pre-commit >= 2.16.0 73 | "bash": f"""\ 74 | VIRTUAL_ENV={shlex.quote(virtualenv)} 75 | PATH={shlex.quote(session.bin)}"{os.pathsep}$PATH" 76 | """, 77 | # pre-commit >= 2.17.0 on Windows forces sh shebang 78 | "/bin/sh": f"""\ 79 | VIRTUAL_ENV={shlex.quote(virtualenv)} 80 | PATH={shlex.quote(session.bin)}"{os.pathsep}$PATH" 81 | """, 82 | } 83 | 84 | hookdir = Path(".git") / "hooks" 85 | if not hookdir.is_dir(): 86 | return 87 | 88 | for hook in hookdir.iterdir(): 89 | if hook.name.endswith(".sample") or not hook.is_file(): 90 | continue 91 | 92 | if not hook.read_bytes().startswith(b"#!"): 93 | continue 94 | 95 | text = hook.read_text() 96 | 97 | if not any(Path("A") == Path("a") and bindir.lower() in text.lower() or bindir in text for bindir in bindirs): 98 | continue 99 | 100 | lines = text.splitlines() 101 | 102 | for executable, header in headers.items(): 103 | if executable in lines[0].lower(): 104 | lines.insert(1, dedent(header)) 105 | hook.write_text("\n".join(lines)) 106 | break 107 | 108 | 109 | @session(name="pre-commit", python=python_versions[0]) 110 | def precommit(session: Session) -> None: 111 | """Lint using pre-commit.""" 112 | args = session.posargs or [ 113 | "run", 114 | "--config=dev_configurations/.pre-commit-config.yaml", 115 | "--all-files", 116 | "--hook-stage=manual", 117 | "--show-diff-on-failure", 118 | ] 119 | session.install( 120 | "black", 121 | "flake8", 122 | "flake8-bandit", 123 | "flake8-bugbear", 124 | "flake8-docstrings", 125 | "flake8-rst-docstrings", 126 | "isort", 127 | "pep8-naming", 128 | "pre-commit", 129 | ) 130 | session.run("pre-commit", *args) 131 | if args and args[0] == "install": 132 | activate_virtualenv_in_precommit_hooks(session) 133 | 134 | 135 | @session(python=python_versions[0]) 136 | def safety(session: Session) -> None: 137 | """Scan dependencies for insecure packages.""" 138 | requirements = session.poetry.export_requirements() 139 | session.install("safety") 140 | session.run("safety", "check", "--full-report", f"--file={requirements}") 141 | 142 | 143 | @session(python=python_versions) 144 | def pyright(session: Session) -> None: 145 | """Type-check using pyright.""" 146 | session.install(".") 147 | session.run("pip", "install", "nox", "nox-poetry", "pytest") 148 | session.chdir(dir="dev_configurations") 149 | session.run("pyright") 150 | 151 | 152 | @session(python=python_versions) 153 | def tests(session: Session) -> None: 154 | """Run the test suite.""" 155 | session.install(".") 156 | session.install("coverage[toml]", "pytest", "pygments") 157 | try: 158 | session.run( 159 | "coverage", 160 | "run", 161 | "--branch", 162 | "--concurrency", 163 | "multiprocessing,thread", 164 | "--parallel", 165 | "-m", 166 | "pytest", 167 | *session.posargs, 168 | ) 169 | finally: 170 | if session.interactive: 171 | session.notify("coverage", posargs=[]) 172 | 173 | 174 | @session(python=python_versions[0]) 175 | def coverage(session: Session) -> None: 176 | """Produce the coverage report.""" 177 | args = session.posargs or ["report"] 178 | 179 | session.install("coverage[toml]") 180 | 181 | if not session.posargs and any(Path().glob(".coverage.*")): 182 | session.run("coverage", "combine") 183 | 184 | session.run("coverage", *args) 185 | 186 | 187 | @session(python=python_versions[0]) 188 | def typeguard(session: Session) -> None: 189 | """Runtime type checking using Typeguard.""" 190 | session.install(".") 191 | session.install("pytest", "typeguard", "pygments") 192 | session.run("pytest", f"--typeguard-packages={package}", *session.posargs) 193 | 194 | 195 | @session(python=python_versions) 196 | def xdoctest(session: Session) -> None: 197 | """Run examples with xdoctest.""" 198 | if session.posargs: 199 | args = [package, *session.posargs] 200 | else: 201 | args = [f"--modname={package}", "--command=all"] 202 | if "FORCE_COLOR" in os.environ: 203 | args.append("--colored=1") 204 | 205 | session.install(".") 206 | session.install("xdoctest[colors]") 207 | session.run("python", "-m", "xdoctest", *args) 208 | 209 | 210 | @session(name="docs-build", python=python_versions[0]) 211 | def docs_build(session: Session) -> None: 212 | """Build the documentation.""" 213 | session.install(".") 214 | session.install("sphinx", "sphinx-click", "furo") 215 | 216 | session.run("sphinx-apidoc", "-f", "-o", "docs/source", "src/") 217 | session.chdir(dir="docs") 218 | 219 | args = session.posargs or ["-b", "html", "-a", "-j", "auto", "source", "build"] 220 | if not session.posargs and "FORCE_COLOR" in os.environ: 221 | args.insert(0, "--color") 222 | 223 | print(f"args: {args}") 224 | 225 | build_dir = Path("docs", "build") 226 | if build_dir.exists(): 227 | shutil.rmtree(build_dir) 228 | 229 | session.run("sphinx-build", *args) 230 | 231 | 232 | @session(python=python_versions[0]) 233 | def docs(session: Session) -> None: 234 | """Build and serve the documentation with live reloading on file changes.""" 235 | session.install(".") 236 | session.install("sphinx", "sphinx-autobuild", "sphinx-click", "furo") 237 | 238 | session.chdir(dir="docs") 239 | 240 | args = session.posargs or ["--open-browser", "source", "build"] 241 | 242 | build_dir = Path("docs", "build") 243 | if build_dir.exists(): 244 | shutil.rmtree(build_dir) 245 | 246 | session.run("sphinx-autobuild", *args) 247 | -------------------------------------------------------------------------------- /docs/guide.md: -------------------------------------------------------------------------------- 1 | --- 2 | substitutions: 3 | HPC: "*Hypermodern Python Cookiecutter*" 4 | --- 5 | 6 | # User Guide 7 | 8 | This is the user guide 9 | for the [Hypermodern Python Cookiecutter], 10 | a Python template based on the [Hypermodern Python] article series. 11 | 12 | If you're in a hurry, check out the [quickstart guide](quickstart) 13 | and the [tutorials](tutorials). 14 | 15 | ## Introduction 16 | 17 | ### About this project 18 | 19 | The {{ HPC }} is a general-purpose template for Python libraries and applications, 20 | released under the [MIT license] 21 | and hosted on [GitHub][hypermodern python cookiecutter]. 22 | 23 | The main objective of this project template is to 24 | enable current best practices 25 | through modern Python tooling. 26 | Our goals are to: 27 | 28 | - focus on simplicity and minimalism, 29 | - promote code quality through automation, and 30 | - provide reliable and repeatable processes, 31 | 32 | all the way from local testing to publishing releases. 33 | 34 | Projects are created from the template using [Cookiecutter], 35 | a project scaffolding tool built on top of the [Jinja] template engine. 36 | 37 | The project template is centered around the following tools: 38 | 39 | - [Poetry] for packaging and dependency management 40 | - [Nox] for automation of checks and other development tasks 41 | - [GitHub Actions] for continuous integration and delivery 42 | 43 | (features)= 44 | 45 | ### Features 46 | 47 | Here is a detailed list of features for this Python template: 48 | 49 | ```{eval-rst} 50 | .. include:: ../README.md 51 | :parser: myst_parser.sphinx_ 52 | :start-after: 53 | :end-before: 54 | 55 | ``` 56 | 57 | ### Version policy 58 | 59 | The {{ HPC }} uses [Calendar Versioning] with a `YYYY.MM.DD` versioning scheme. 60 | 61 | The current stable release is [2022.6.3]. 62 | 63 | (installation)= 64 | 65 | ## Installation 66 | 67 | ### System requirements 68 | 69 | You need a recent Windows, Linux, Unix, or Mac system with [git] installed. 70 | 71 | :::{note} 72 | When working with this template on Windows, 73 | configure your text editor or IDE 74 | to use only [UNIX-style line endings] (line feeds). 75 | 76 | The project template contains a [.gitattributes] file 77 | which enables end-of-line normalization for your entire working tree. 78 | Additionally, the [Prettier] code formatter converts line endings to line feeds. 79 | Windows-style line endings (`CRLF`) should therefore never make it into your Git repository. 80 | 81 | Nonetheless, configuring your editor for line feeds is recommended 82 | to avoid complaints from the [pre-commit] hook for Prettier. 83 | ::: 84 | 85 | ### Getting Python (Windows) 86 | 87 | If you're on Windows, 88 | download the recommended installer for the latest stable release of Python 89 | from the official [Python website]. 90 | Before clicking **Install now**, 91 | enable the option to add Python to your `PATH` environment variable. 92 | 93 | Verify your installation by checking the output of the following commands in a new terminal window: 94 | 95 | ``` 96 | python -VV 97 | py -VV 98 | ``` 99 | 100 | Both of these commands should display the latest Python version, 3.10. 101 | 102 | For local testing with multiple Python versions, 103 | repeat these steps for the latest bugfix releases of Python 3.7+, 104 | with the following changes: 105 | 106 | - Do _not_ enable the option to add Python to the `PATH` environment variable. 107 | - `py -VV` and `python -VV` should still display the version of the latest stable release. 108 | - `py -X.Y -VV` (e.g. `py -3.7 -VV`) should display the exact version you just installed. 109 | 110 | Note that binary installers are not provided for security releases. 111 | 112 | ### Getting Python (Mac, Linux, Unix) 113 | 114 | If you're on a Mac, Linux, or Unix system, 115 | use [pyenv] for installing and managing Python versions. 116 | Please refer to the documentation of this project 117 | for detailed installation and usage instructions. 118 | (The following instructions assume that 119 | your system already has [bash] and [curl] installed.) 120 | 121 | Install [pyenv] like this: 122 | 123 | ```console 124 | $ curl https://pyenv.run | bash 125 | ``` 126 | 127 | Add the following lines to your `~/.bashrc`: 128 | 129 | ```sh 130 | export PATH="$HOME/.pyenv/bin:$PATH" 131 | eval "$(pyenv init -)" 132 | eval "$(pyenv virtualenv-init -)" 133 | ``` 134 | 135 | Install the Python build dependencies for your platform, 136 | using one of the commands listed in the [official instructions][pyenv wiki]. 137 | 138 | Install the latest point release of every supported Python version. 139 | This project template supports Python 3.7, 3.8, 3.9, and 3.10. 140 | 141 | ```console 142 | $ pyenv install 3.7.12 143 | $ pyenv install 3.8.12 144 | $ pyenv install 3.9.10 145 | $ pyenv install 3.10.2 146 | ``` 147 | 148 | After creating your project (see [below](creating-a-project)), 149 | you can make these Python versions accessible in the project directory, 150 | using the following command: 151 | 152 | ```console 153 | $ pyenv local 3.10.2 3.9.10 3.8.12 3.7.12 154 | ``` 155 | 156 | The first version listed is the one used when you type plain `python`. 157 | Every version can be used by invoking `python`. 158 | For example, use `python3.7` to invoke Python 3.7. 159 | 160 | ### Requirements 161 | 162 | :::{note} 163 | It is recommended to use [pipx] to install Python tools 164 | which are not specific to a single project. 165 | Please refer to the official documentation 166 | for detailed installation and usage instructions. 167 | If you decide to skip `pipx` installation, 168 | use [pip install] with the `--user` option instead. 169 | ::: 170 | 171 | You need four tools to use this template: 172 | 173 | - [Cookiecutter] to create projects from the template, 174 | - [Poetry] to manage packaging and dependencies 175 | - [Nox] to automate checks and other tasks 176 | - [nox-poetry] for using Poetry in Nox sessions 177 | 178 | Install [Cookiecutter] using pipx: 179 | 180 | ```console 181 | $ pipx install cookiecutter 182 | ``` 183 | 184 | Install [Poetry] by downloading and running [install-poetry.py]: 185 | 186 | ```console 187 | $ python install-poetry.py 188 | ``` 189 | 190 | Install [Nox] and [nox-poetry] using pipx: 191 | 192 | ```console 193 | $ pipx install nox 194 | $ pipx inject nox nox-poetry 195 | ``` 196 | 197 | Remember to upgrade these tools regularly: 198 | 199 | ```console 200 | $ pipx upgrade cookiecutter 201 | $ pipx upgrade --include-injected nox 202 | $ poetry self update 203 | ``` 204 | 205 | ## Project creation 206 | 207 | (creating-a-project)= 208 | 209 | ### Creating a project 210 | 211 | Create a project from this template 212 | by pointing Cookiecutter to its [GitHub repository][hypermodern python cookiecutter]. 213 | Use the `--checkout` option with the [current stable release][2022.6.3]: 214 | 215 | ```console 216 | $ cookiecutter gh:cjolowicz/cookiecutter-hypermodern-python \ 217 | --checkout="2022.6.3" 218 | ``` 219 | 220 | Cookiecutter downloads the template, 221 | and asks you a series of questions about project variables, 222 | for example, how you wish your project to be named. 223 | When you have answered these questions, 224 | your project is generated in the current directory, 225 | using a subdirectory with the same name as your project. 226 | 227 | Here is a complete list of the project variables defined by this template: 228 | 229 | :::{list-table} Project variables 230 | :header-rows: 1 231 | :widths: auto 232 | 233 | - - Variable 234 | - Description 235 | - Example 236 | - - `project_name` 237 | - Project name on PyPI and GitHub 238 | - `hypermodern-python` 239 | - - `package_name` 240 | - Import name of the package 241 | - `hypermodern_python` 242 | - - `friendly_name` 243 | - Friendly project name 244 | - `Hypermodern Python` 245 | - - `author` 246 | - Primary author 247 | - Katherine Johnson 248 | - - `email` 249 | - E-mail address of the author 250 | - katherine@example.com 251 | - - `github_user` 252 | - GitHub username of the author 253 | - `katherine` 254 | - - `version` 255 | - Initial project version 256 | - `0.0.0` 257 | - - `copyright_year` 258 | - The project copyright year 259 | - `2022` 260 | - - `license` 261 | - The project license 262 | - `MIT` 263 | - - `development_status` 264 | - Development status of the project 265 | - `Development Status :: 3 - Alpha` 266 | 267 | ::: 268 | 269 | :::{note} 270 | The initial project version should be the latest release on [PyPI], 271 | or `0.0.0` for an unreleased package. 272 | See [The Release workflow](the-release-workflow) for details. 273 | ::: 274 | 275 | Your choices are recorded in the file `.cookiecutter.json` in the generated project, 276 | together with the URL of this Cookiecutter template. 277 | Having this [JSON] file in the project makes it possible later on 278 | to update your project with changes from the Cookiecutter template, 279 | using tools such as [cupper]. 280 | 281 | In the remainder of this guide, 282 | `` and `` are used 283 | to refer to the project and package names, respectively. 284 | By default, their only difference is that 285 | the project name uses hyphens (_kebab case_), 286 | whereas the package name uses underscores (_snake case_). 287 | 288 | ### Uploading to GitHub 289 | 290 | This project template is designed for use with [GitHub]. 291 | After generating the project, 292 | your next steps are to create a Git repository and upload it to GitHub. 293 | 294 | Change to the root directory of your new project, 295 | initialize a Git repository, and 296 | create a commit for the initial project structure. 297 | In the commands below, 298 | replace `` by the name of your project. 299 | 300 | ```console 301 | $ cd 302 | $ git init 303 | $ git add . 304 | $ git commit 305 | ``` 306 | 307 | Use the following command to ensure your default branch is called `main`, 308 | which is the [default branch name for GitHub repositories][github renaming]. 309 | 310 | ```console 311 | $ git branch --move --force main 312 | ``` 313 | 314 | Create an empty repository on [GitHub], 315 | using the project name you chose when you generated the project. 316 | 317 | :::{note} 318 | Do not include a `README.md`, `LICENSE`, or `.gitignore`. 319 | These files are provided by the project template. 320 | ::: 321 | 322 | Finally, upload your repository to GitHub. 323 | In the commands below, replace `` by your GitHub username, 324 | and `` by the name of your project. 325 | 326 | ```console 327 | $ git remote add origin git@github.com:/.git 328 | $ git push --set-upstream origin main 329 | ``` 330 | 331 | Now may be a good time to set up Continuous Integration for your repository. 332 | Refer to the section [External services](external-services) 333 | for detailed instructions. 334 | 335 | ## Project overview 336 | 337 | ### Files and directories 338 | 339 | This section provides an overview of all the files generated for your project. 340 | 341 | Let's start with the directory layout: 342 | 343 | :::{list-table} Directories 344 | :widths: auto 345 | 346 | - - `src/` 347 | - Python package 348 | - - `tests` 349 | - Test suite 350 | - - `docs` 351 | - Documentation 352 | - - `.github/workflows` 353 | - GitHub Actions workflows 354 | 355 | ::: 356 | 357 | The Python package is located in the `src/` directory. 358 | For more details on these files, refer to the section [The initial package](the-initial-package). 359 | 360 | :::{list-table} Python package 361 | :widths: auto 362 | 363 | - - `src//py.typed` 364 | - Marker file for [PEP 561][pep 561] 365 | - - `src//__init__.py` 366 | - Package initialization 367 | - - `src//__main__.py` 368 | - Command-line interface 369 | 370 | ::: 371 | 372 | The test suite is located in the `tests` directory. 373 | For more details on these files, refer to the section [The test suite](the-test-suite). 374 | 375 | :::{list-table} Test suite 376 | :widths: auto 377 | 378 | - - `tests/__init__.py` 379 | - Test package initialization 380 | - - `tests/test_main.py` 381 | - Test cases for `__main__` 382 | 383 | ::: 384 | 385 | The project documentation is written in [Markdown]. 386 | The documentation files in the top-level directory are rendered on [GitHub]: 387 | 388 | :::{list-table} Documentation files (top-level) 389 | :widths: auto 390 | 391 | - - `README.md` 392 | - Project description for GitHub and PyPI 393 | - - `CONTRIBUTING.md` 394 | - Contributor Guide 395 | - - `CODE_OF_CONDUCT.md` 396 | - Code of Conduct 397 | - - `LICENSE` 398 | - License 399 | 400 | ::: 401 | 402 | The files in the `docs` directory are 403 | built using [Sphinx](documentation) and [MyST]. 404 | The Sphinx documentation is hosted on [Read the Docs](read-the-docs-integration): 405 | 406 | :::{list-table} Documentation files (Sphinx) 407 | :widths: auto 408 | 409 | - - `index.md` 410 | - Main document 411 | - - `contributing.md` 412 | - Contributor Guide (via include) 413 | - - `codeofconduct.md` 414 | - Code of Conduct (via include) 415 | - - `license.md` 416 | - License (via include) 417 | - - `reference.md` 418 | - API reference 419 | - - `usage.md` 420 | - Command-line reference 421 | 422 | ::: 423 | 424 | The `.github/workflows` directory contains the [GitHub Actions workflows](github-actions-workflows): 425 | 426 | :::{list-table} GitHub Actions workflows 427 | :widths: auto 428 | 429 | - - `release.yml` 430 | - [The Release workflow](the-release-workflow) 431 | - - `tests.yml` 432 | - [The Tests workflow](the-tests-workflow) 433 | - - `labeler.yml` 434 | - [The Labeler workflow](the-labeler-workflow) 435 | 436 | ::: 437 | 438 | The project contains many configuration files for developer tools. 439 | Most of these are located in the top-level directory. 440 | The table below lists these files, 441 | and links each file to a section with more details. 442 | 443 | :::{list-table} Configuration files 444 | :widths: auto 445 | 446 | - - `.cookiecutter.json` 447 | - [Project variables](creating-a-project) 448 | - - `.darglint` 449 | - Configuration for [darglint](darglint-integration) 450 | - - `.github/dependabot.yml` 451 | - Configuration for [Dependabot](dependabot-integration) 452 | - - `.flake8` 453 | - Configuration for [Flake8](the-flake8-hook) 454 | - - `.gitattributes` 455 | - [Git attributes][.gitattributes] 456 | - - `.gitignore` 457 | - [Git ignore file][.gitignore] 458 | - - `.github/release-drafter.yml` 459 | - Configuration for [Release Drafter](the-release-workflow) 460 | - - `.github/labels.yml` 461 | - Configuration for [GitHub Labeler](the-labeler-workflow) 462 | - - `.pre-commit-config.yaml` 463 | - Configuration for [pre-commit](linting-with-pre-commit) 464 | - - `.readthedocs.yml` 465 | - Configuration for [Read the Docs](read-the-docs-integration) 466 | - - `codecov.yml` 467 | - Configuration for [Codecov](codecov-integration) 468 | - - `docs/conf.py` 469 | - Configuration for [Sphinx](documentation) 470 | - - `noxfile.py` 471 | - Configuration for [Nox](using-nox) 472 | - - `pyproject.toml` 473 | - Configuration for [Poetry](using-poetry), 474 | [Coverage.py](the-coverage-session), 475 | [isort](the-isort-hook), 476 | and [mypy](type-checking-with-mypy) 477 | 478 | ::: 479 | 480 | The `pyproject.toml` file is described in more detail [below](the-pyproject-toml-file). 481 | 482 | [Dependencies](managing-dependencies) are managed by [Poetry] 483 | and declared in the [pyproject.toml](the-pyproject-toml-file) file. 484 | The table below lists some additional files with pinned dependencies. 485 | Follow the links for more details on these. 486 | 487 | :::{list-table} Dependency files 488 | :widths: auto 489 | 490 | - - `poetry.lock` 491 | - [Poetry lock file](the-lock-file) 492 | - - `docs/requirements.txt` 493 | - Requirements file for [Read the Docs](read-the-docs-integration) 494 | - - `.github/workflows/constraints.txt` 495 | - Constraints file for [GitHub Actions workflows](workflow-constraints) 496 | 497 | ::: 498 | 499 | (the-initial-package)= 500 | 501 | ### The initial package 502 | 503 | You can find the initial Python package in your generated project 504 | under the `src` directory: 505 | 506 | ``` 507 | src 508 | └── 509 | ├── __init__.py 510 | ├── __main__.py 511 | └── py.typed 512 | ``` 513 | 514 | 515 | 516 | `__init__.py` 517 | 518 | : This file declares the directory as a [Python package], 519 | and contains any package initialization code. 520 | 521 | `__main__.py` 522 | 523 | : The [`__main__`][__main__] module defines the entry point for the command-line interface. 524 | The command-line interface is implemented using the [Click] library, 525 | and supports `--help` and `--version` options. 526 | When the package is installed, 527 | a script named `` is placed 528 | in the Python installation or virtual environment. 529 | This allows you to invoke the command-line interface using only the project name: 530 | 531 | ```console 532 | $ poetry run # during development 533 | $ # after installation 534 | ``` 535 | 536 | The command-line interface can also be invoked 537 | by specifying a Python interpreter and the package name: 538 | 539 | ```console 540 | $ python -m [] 541 | ``` 542 | 543 | `py.typed` 544 | 545 | : This is an empty marker file, 546 | which declares that your package supports typing 547 | and is distributed with its own type information 548 | ([PEP 561][pep 561]). 549 | This allows people using your package 550 | to type-check their Python code against it. 551 | 552 | 553 | 554 | (the-test-suite)= 555 | 556 | ### The test suite 557 | 558 | Tests are written using the [pytest] testing framework, 559 | the _de facto_ standard for testing in Python. 560 | 561 | The test suite is located in the `tests` directory: 562 | 563 | ``` 564 | tests 565 | ├── __init__.py 566 | └── test_main.py 567 | ``` 568 | 569 | The test suite is [declared as a package][pytest layout], 570 | and mirrors the source layout of the package under test. 571 | The file `test_main.py` contains tests for the `__main__` module. 572 | 573 | Initially, the test suite contains a single test case, 574 | checking whether the program exits with a status code of zero. 575 | It also provides a [test fixture] using [click.testing.CliRunner], 576 | a helper class for invoking the program from within tests. 577 | 578 | For details on how to run the test suite, 579 | refer to the section [The tests session](the-tests-session). 580 | 581 | (documentation)= 582 | 583 | ### Documentation 584 | 585 | The project documentation is written in [Markdown] 586 | and processed by the [Sphinx] documentation engine using the [MyST] extension. 587 | 588 | The top-level directory contains several stand-alone documentation files: 589 | 590 | 591 | 592 | `README.md` 593 | 594 | : This file is your main project page and displayed on GitHub and PyPI. 595 | 596 | `CONTRIBUTING.md` 597 | 598 | : The Contributor Guide explains how other people can contribute to your project. 599 | 600 | `CODE_OF_CONDUCT.md` 601 | 602 | : The Code of Conduct outlines the behavior 603 | expected from participants of your project. 604 | It is adapted from the [Contributor Covenant], version 2.1. 605 | 606 | `LICENSE.md` 607 | 608 | : This file contains the text of your project's license. 609 | 610 | :::{note} 611 | The files above are also rendered on GitHub and PyPI. 612 | Keep them in plain Markdown, without [MyST] syntax extensions. 613 | ::: 614 | 615 | The documentation files in the `docs` directory are built using [Sphinx] and [MyST]: 616 | 617 | `index.md` 618 | 619 | : This is the main documentation page. 620 | It includes the project description from `README.md`. 621 | This file also defines the navigation menu, 622 | with links to other documentation pages. 623 | The *Changelog* menu entry 624 | links to the [GitHub Releases][github release] page of your project. 625 | 626 | `contributing.md` 627 | 628 | : This file includes the Contributor Guide from `CONTRIBUTING.md`. 629 | 630 | `codeofconduct.md` 631 | 632 | : This file includes the Code of Conduct from `CODE_OF_CONDUCT.md`. 633 | 634 | `license.md` 635 | 636 | : This file includes the license from `LICENSE.md`. 637 | 638 | `reference.md` 639 | 640 | : The API reference for your project. 641 | It is generated from docstrings and type annotations in the source code, 642 | using the [autodoc] and [napoleon] extensions. 643 | 644 | `usage.md` 645 | 646 | : The command-line reference for your project. 647 | It is generated by inspecting the [click] entry-point in your package, 648 | using the [sphinx-click] extension. 649 | 650 | The `docs` directory contains two more files: 651 | 652 | `conf.py` 653 | 654 | : This Python file contains the [Sphinx configuration]. 655 | 656 | `requirements.txt` 657 | 658 | : The requirements file pins the build dependencies for the Sphinx documentation. 659 | This file is only used on Read the Docs. 660 | 661 | 662 | 663 | The project documentation is built and hosted on 664 | [Read the Docs](read-the-docs-integration), 665 | and uses the [furo] Sphinx theme. 666 | 667 | You can also build the documentation locally using Nox, 668 | see [The docs session](the-docs-session). 669 | 670 | ## Packaging 671 | 672 | (the-pyproject-toml-file)= 673 | 674 | ### The pyproject.toml file 675 | 676 | The configuration file for the Python package is located 677 | in the root directory of the project, 678 | and named `pyproject.toml`. 679 | It uses the [TOML] configuration file format, 680 | and contains two sections---_tables_ in TOML parlance---, 681 | specified in [PEP 517][pep 517] and [518][pep 518]: 682 | 683 | - The `build-system` table 684 | declares the requirements and the entry point 685 | used to build a distribution package for the project. 686 | This template uses [Poetry] as the build system. 687 | - The `tool` table contains sub-tables 688 | where tools can store configuration under their [PyPI] name. 689 | 690 | :::{list-table} Tool configurations in pyproject.toml 691 | :widths: auto 692 | 693 | - - `tool.coverage` 694 | - Configuration for [Coverage.py] 695 | - - `tool.isort` 696 | - Configuration for [isort] 697 | - - `tool.mypy` 698 | - Configuration for [mypy] 699 | - - `tool.poetry` 700 | - Configuration for [Poetry] 701 | 702 | ::: 703 | 704 | The `tool.poetry` table 705 | contains the metadata for your package, 706 | such as its name, version, and authors, 707 | as well as the list of dependencies for the package. 708 | Please refer to the [Poetry documentation][pyproject.toml] 709 | for a detailed description of each configuration key. 710 | 711 | (version-constraints)= 712 | 713 | ### Version constraints 714 | 715 | :::{admonition} TL;DR 716 | This project template omits upper bounds from all version constraints. 717 | 718 | You are encouraged to manually remove upper bounds 719 | for dependencies you add to your project using Poetry: 720 | 721 | 1. Replace `^1.2.3` with `>=1.2.3` in `pyproject.toml` 722 | 2. Run `poetry lock --no-update` to update `poetry.lock` 723 | 724 | ::: 725 | 726 | [Version constraints][versions and constraints] express 727 | which versions of dependencies are compatible with your project. 728 | In the case of core dependencies, 729 | they are also a part of distribution packages, 730 | and as such affect end-users of your package. 731 | 732 | :::{note} 733 | Dependencies are Python packages used by your project, 734 | and they come in two types: 735 | 736 | - _Core dependencies_ are required by users running your code, 737 | and typically consist of third-party libraries imported by your package. 738 | When your package is distributed, 739 | the [package metadata] includes these dependencies, 740 | allowing tools like [pip] to automatically install them alongside your package. 741 | - _Development dependencies_ are only required by developers working on your code. 742 | Examples are applications used to run tests, 743 | check code for style and correctness, 744 | or to build documentation. 745 | These dependencies are not a part of distribution packages, 746 | because users do not require them to run your code. 747 | 748 | ::: 749 | 750 | For every dependency added to your project, 751 | Poetry writes a version constraint to `pyproject.toml`. 752 | Dependencies are kept in two TOML tables: 753 | 754 | - `tool.poetry.dependencies`---for core dependencies 755 | - `tool.poetry.dev-dependencies`---for development dependencies 756 | 757 | By default, version constraints added by Poetry have both a lower and an upper bound: 758 | 759 | - The lower bound requires users of your package to have at least the version 760 | that was current when you added the dependency. 761 | - The upper bound allows users to upgrade to newer releases of dependencies, 762 | as long as the version number does not indicate a breaking change. 763 | 764 | According to the [Semantic Versioning] standard, 765 | only major releases may contain breaking changes, 766 | once a project has reached version 1.0.0. 767 | A major release is one that increments the major version 768 | (the first component of the version identifier). 769 | An example for such a version constraint would be `^1.2.3`, 770 | which is a Poetry-specific shorthand equivalent to `>= 1.2.3, < 2`. 771 | 772 | This project template omits upper bounds from all version constraints, 773 | in a conscious departure from Poetry's defaults. 774 | There are two separate reasons for removing version caps, 775 | one principled, the other pragmatic: 776 | 777 | 1. Version caps lead to problems in the Python ecosystem due to its flat dependency management. 778 | 2. Version caps lead to frequent merge conflicts between dependency updates. 779 | 780 | The first point is treated in detail in the following articles: 781 | 782 | - [Should You Use Upper Bound Version Constraints?][schreiner constraints] and [Poetry Versions][schreiner poetry] by Henry Schreiner 783 | - [Semantic Versioning Will Not Save You][schlawack semantic] by Hynek Schlawack 784 | - [Version numbers: how to use them?][gabor version] by Bernát Gábor 785 | - [Why I don't like SemVer anymore][cannon semver] by Brett Cannon 786 | 787 | The second point is ultimately due to the fact that 788 | every updated version constraint changes a hashsum in the `poetry.lock` file. 789 | This means that PRs updating version constraints will _always_ conflict with each other. 790 | 791 | :::{note} 792 | The problem with merge conflicts is greatly exacerbated by a [Dependabot issue][dependabot issue 4435]: 793 | Dependabot updates version constraints in `pyproject.toml` 794 | even when the version constraint already covered the new version. 795 | This can be avoided using a configuration setting 796 | where only the lock file is ever updated, not the version constraints. 797 | Omitting version caps makes the lockfile-only strategy a viable alternative. 798 | ::: 799 | 800 | Poetry will still add `^1.2.3`-style version constraints whenever you add a dependency. 801 | You should edit the version constraint in `pyproject.toml`, 802 | replacing `^1.2.3` with `>=1.2.3` to remove the upper bound. 803 | Then update the lock file by invoking `poetry lock --no-update`. 804 | 805 | (the-lock-file)= 806 | 807 | ### The lock file 808 | 809 | Poetry records the exact version of each direct and indirect dependency 810 | in its lock file, named `poetry.lock` and located in the root directory of the project. 811 | The lock file does not affect users of the package, 812 | because its contents are not included in distribution packages. 813 | 814 | The lock file is useful for a number of reasons: 815 | 816 | - It ensures that local checks run in the same environment as on the CI server, 817 | making the CI predictable and deterministic. 818 | - When collaborating with other developers, 819 | it allows everybody to use the same development environment. 820 | - When deploying an application, the lock file helps you 821 | keep production and development environments as similar as possible 822 | ([dev-prod parity]). 823 | 824 | For these reasons, the lock file should be kept under source control. 825 | 826 | ### Dependencies 827 | 828 | This project template has a core dependency on [Click], 829 | a library for creating command-line interfaces. 830 | The template also comes with various development dependencies. 831 | See the table below for an overview of the dependencies of generated projects: 832 | 833 | :::{list-table} Dependencies 834 | :widths: auto 835 | 836 | - - [black] 837 | - The uncompromising code formatter. 838 | - - [click] 839 | - Composable command line interface toolkit 840 | - - [coverage][coverage.py] 841 | - Code coverage measurement for Python 842 | - - [darglint] 843 | - A utility for ensuring Google-style docstrings stay up to date with the source code. 844 | - - [flake8] 845 | - the modular source code checker: pep8 pyflakes and co 846 | - - [flake8-bandit] 847 | - Automated security testing with bandit and flake8. 848 | - - [flake8-bugbear] 849 | - A plugin for flake8 finding likely bugs and design problems in your program. 850 | - - [flake8-docstrings] 851 | - Extension for flake8 which uses pydocstyle to check docstrings 852 | - - [flake8-rst-docstrings] 853 | - Python docstring reStructuredText (RST) validator 854 | - - [furo] 855 | - A clean customisable Sphinx documentation theme. 856 | - - [isort] 857 | - A Python utility / library to sort Python imports. 858 | - - [mypy] 859 | - Optional static typing for Python 860 | - - [pep8-naming] 861 | - Check PEP-8 naming conventions, plugin for flake8 862 | - - [pre-commit] 863 | - A framework for managing and maintaining multi-language pre-commit hooks. 864 | - - [pre-commit-hooks] 865 | - Some out-of-the-box hooks for pre-commit. 866 | - - [pygments] 867 | - Pygments is a syntax highlighting package written in Python. 868 | - - [pytest] 869 | - pytest: simple powerful testing with Python 870 | - - [pyupgrade] 871 | - A tool to automatically upgrade syntax for newer versions. 872 | - - [safety] 873 | - Checks installed dependencies for known vulnerabilities. 874 | - - [sphinx] 875 | - Python documentation generator 876 | - - [sphinx-autobuild] 877 | - Rebuild Sphinx documentation on changes, with live-reload in the browser. 878 | - - [sphinx-click] 879 | - Sphinx extension that automatically documents click applications 880 | - - [typeguard] 881 | - Run-time type checker for Python 882 | - - [xdoctest] 883 | - A rewrite of the builtin doctest module 884 | 885 | ::: 886 | 887 | (using-poetry)= 888 | 889 | ## Using Poetry 890 | 891 | [Poetry] manages packaging and dependencies for Python projects. 892 | 893 | (managing-dependencies)= 894 | 895 | ### Managing dependencies 896 | 897 | Use the command [poetry show] to 898 | see the full list of direct and indirect dependencies of your package: 899 | 900 | ```console 901 | $ poetry show 902 | ``` 903 | 904 | Use the command [poetry add] to add a dependency for your package: 905 | 906 | ```console 907 | $ poetry add foobar # for core dependencies 908 | $ poetry add --dev foobar # for development dependencies 909 | ``` 910 | 911 | :::{important} 912 | It is recommended to remove the upper bound from the version constraint added by Poetry: 913 | 914 | 1. Edit `pyproject.toml` to replace `^1.2.3` with `>=1.2.3` in the dependency entry 915 | 2. Update `poetry.lock` using the command `poetry lock --no-update` 916 | 917 | See [Version constraints](version-constraints) for more details. 918 | ::: 919 | 920 | Use the command [poetry remove] to remove a dependency from your package: 921 | 922 | ```console 923 | $ poetry remove foobar 924 | ``` 925 | 926 | Use the command [poetry update] to upgrade the dependency to a new release: 927 | 928 | ```console 929 | $ poetry update foobar 930 | ``` 931 | 932 | :::{note} 933 | Dependencies in the {{ HPC }} are managed by [Dependabot](dependabot-integration). 934 | When newer versions of dependencies become available, 935 | Dependabot updates the `poetry.lock` file and submits a pull request. 936 | ::: 937 | 938 | ### Installing the package for development 939 | 940 | Poetry manages a virtual environment for your project, 941 | which contains your package, its core dependencies, and the development dependencies. 942 | All dependencies are kept at the versions specified by the lock file. 943 | 944 | :::{note} 945 | A [virtual environment] gives your project 946 | an isolated runtime environment, 947 | consisting of a specific Python version and 948 | an independent set of installed Python packages. 949 | This way, the dependencies of your current project 950 | do not interfere with the system-wide Python installation, 951 | or other projects you're working on. 952 | ::: 953 | 954 | You can install your package and its dependencies 955 | into Poetry's virtual environment 956 | using the command [poetry install]. 957 | 958 | ```console 959 | $ poetry install 960 | ``` 961 | 962 | This command performs a so-called [editable install] of your package: 963 | Instead of building and installing a distribution package, 964 | it creates a special `.egg-link` file that links to your local source code. 965 | This means that code edits are directly visible in the environment 966 | without the need to reinstall your package. 967 | 968 | Installing your package implicitly creates the virtual environment 969 | if it does not exist yet, 970 | using the currently active Python interpreter, 971 | or the first one found 972 | which satisfies the Python versions supported by your project. 973 | 974 | ### Managing environments 975 | 976 | You can create environments explicitly 977 | with the [poetry env] command, 978 | specifying the desired Python version. 979 | This allows you to create an environment 980 | for every Python version supported by your project, 981 | and easily switch between them: 982 | 983 | ```console 984 | $ poetry env use 3.7 985 | $ poetry env use 3.8 986 | $ poetry env use 3.9 987 | $ poetry env use 3.10 988 | ``` 989 | 990 | Only one Poetry environment can be active at any time. 991 | Note that `3.10` comes last, 992 | to ensure that the current Python release is the active environment. 993 | Install your package with `poetry install` into each environment after creating it. 994 | 995 | Use the command `poetry env list` to list the available environments: 996 | 997 | ```console 998 | $ poetry env list 999 | ``` 1000 | 1001 | Use the command `poetry env remove` to remove an environment: 1002 | 1003 | ```console 1004 | $ poetry env remove 1005 | ``` 1006 | 1007 | Use the command `poetry env info` to show information about the active environment: 1008 | 1009 | ```console 1010 | $ poetry env info 1011 | ``` 1012 | 1013 | ### Running commands 1014 | 1015 | You can run an interactive Python session inside the active environment 1016 | using the command [poetry run]: 1017 | 1018 | ```console 1019 | $ poetry run python 1020 | ``` 1021 | 1022 | The same command allows you to invoke the command-line interface of your project: 1023 | 1024 | ```console 1025 | $ poetry run 1026 | ``` 1027 | 1028 | You can also run developer tools, such as [pytest]: 1029 | 1030 | ```console 1031 | $ poetry run pytest 1032 | ``` 1033 | 1034 | While it is handy to have developer tools available in the Poetry environment, 1035 | it is usually recommended to run these using Nox, 1036 | as described in the section [Using Nox](using-nox). 1037 | 1038 | ### Building and distributing the package 1039 | 1040 | :::{note} 1041 | With the {{ HPC }}, 1042 | building and distributing your package 1043 | is taken care of by [GitHub Actions]. 1044 | For more information, 1045 | see the section [The Release workflow](the-release-workflow). 1046 | ::: 1047 | 1048 | This section gives a short overview of 1049 | how you can build and distribute your package 1050 | from the command line, 1051 | using the following Poetry commands: 1052 | 1053 | ```console 1054 | $ poetry build 1055 | $ poetry publish 1056 | ``` 1057 | 1058 | Building the package is done with the [python build] command, 1059 | which generates _distribution packages_ 1060 | in the `dist` directory of your project. 1061 | These are compressed archives that 1062 | an end-user can download and install on their system. 1063 | They come in two flavours: 1064 | source (or _sdist_) archives, and 1065 | binary packages in the [wheel] format. 1066 | 1067 | Publishing the package is done with the [python publish] command, 1068 | which uploads the distribution packages 1069 | to your account on [PyPI], 1070 | the official Python package registry. 1071 | 1072 | ### Installing the package 1073 | 1074 | Once your package is on PyPI, 1075 | others can install it with [pip], [pipx], or Poetry: 1076 | 1077 | ```console 1078 | $ pip install 1079 | $ pipx install 1080 | $ poetry add 1081 | ``` 1082 | 1083 | While [pip] is the workhorse of the Python packaging ecosystem, 1084 | you should use higher-level tools to install your package: 1085 | 1086 | - If the package is an application, install it with [pipx]. 1087 | - If the package is a library, install it with [poetry add] in other projects. 1088 | 1089 | The primary benefit of these installation methods is that 1090 | your package is installed into an isolated environment, 1091 | without polluting the system environment, 1092 | or the environments of other applications. 1093 | This way, 1094 | applications can use specific versions of their direct and indirect dependencies, 1095 | without getting in each other's way. 1096 | 1097 | If the other project is not managed by Poetry, 1098 | use whatever package manager the other project uses. 1099 | You can always install your project into a virtual environment with plain [pip]. 1100 | 1101 | (using-nox)= 1102 | 1103 | ## Using Nox 1104 | 1105 | [Nox] automates testing in multiple Python environments. 1106 | Like its older sibling [tox], 1107 | Nox makes it easy to run any kind of job in an isolated environment, 1108 | with only those dependencies installed that the job needs. 1109 | 1110 | Nox sessions are defined in a Python file 1111 | named `noxfile.py` and located in the project directory. 1112 | They consist of a virtual environment 1113 | and a set of commands to run in that environment. 1114 | 1115 | While Poetry environments allow you to 1116 | interact with your package during development, 1117 | Nox environments are used to run developer tools 1118 | in a reliable and repeatable way across Python versions. 1119 | 1120 | Most sessions are run with every supported Python version. 1121 | Other sessions are only run with the current stable Python version, 1122 | for example the session used to build the documentation. 1123 | 1124 | ### Running sessions 1125 | 1126 | If you invoke Nox by itself, it will run the full test suite: 1127 | 1128 | ```console 1129 | $ nox 1130 | ``` 1131 | 1132 | This includes tests, linters, type checks, and more. 1133 | For the full list, please refer to the table [below](table-of-nox-sessions). 1134 | 1135 | The list of sessions run by default can be configured 1136 | by editing `nox.options.sessions` in `noxfile.py`. 1137 | Currently the list only excludes the [docs session](the-docs-session) 1138 | (which spawns an HTTP server) 1139 | and the [coverage session](the-coverage-session) 1140 | (which is triggered by the [tests session](the-tests-session)). 1141 | 1142 | You can also run a specific Nox session, using the `--session` option. 1143 | For example, build the documentation like this: 1144 | 1145 | ```console 1146 | $ nox --session=docs 1147 | ``` 1148 | 1149 | Print a list of the available Nox sessions 1150 | using the `--list-sessions` option: 1151 | 1152 | ```console 1153 | $ nox --list-sessions 1154 | ``` 1155 | 1156 | Nox creates virtual environments from scratch on each invocation. 1157 | You can speed things up by passing the 1158 | [--reuse-existing-virtualenvs] option, 1159 | or the equivalent short option `-r`. 1160 | For example, the following may be more practical during development 1161 | (this will only run tests and type checks, on the current Python release): 1162 | 1163 | ```console 1164 | $ nox -p 3.10 -rs tests mypy 1165 | ``` 1166 | 1167 | Many sessions accept additional options after `--` separator. 1168 | For example, the following command runs a specific test module: 1169 | 1170 | ```console 1171 | $ nox --session=tests -- tests/test_main.py 1172 | ``` 1173 | 1174 | ### Overview of Nox sessions 1175 | 1176 | (table-of-nox-sessions)= 1177 | 1178 | The following table gives an overview of the available Nox sessions: 1179 | 1180 | :::{list-table} Nox sessions 1181 | :header-rows: 1 1182 | :widths: auto 1183 | 1184 | - - Session 1185 | - Description 1186 | - Python 1187 | - Default 1188 | - - [coverage](the-coverage-session) 1189 | - Report coverage with [Coverage.py] 1190 | - `3.10` 1191 | - (✓) 1192 | - - [docs](the-docs-session) 1193 | - Build and serve [Sphinx] documentation 1194 | - `3.10` 1195 | - 1196 | - - [docs-build](the-docs-build-session) 1197 | - Build [Sphinx] documentation 1198 | - `3.10` 1199 | - ✓ 1200 | - - [mypy](the-mypy-session) 1201 | - Type-check with [mypy] 1202 | - `3.7` … `3.10` 1203 | - ✓ 1204 | - - [pre-commit](the-pre-commit-session) 1205 | - Lint with [pre-commit] 1206 | - `3.10` 1207 | - ✓ 1208 | - - [safety](the-safety-session) 1209 | - Scan dependencies with [Safety] 1210 | - `3.10` 1211 | - ✓ 1212 | - - [tests](the-tests-session) 1213 | - Run tests with [pytest] 1214 | - `3.7` … `3.10` 1215 | - ✓ 1216 | - - [typeguard](the-typeguard-session) 1217 | - Type-check with [Typeguard] 1218 | - `3.10` 1219 | - ✓ 1220 | - - [xdoctest](the-xdoctest-session) 1221 | - Run examples with [xdoctest] 1222 | - `3.7` … `3.10` 1223 | - ✓ 1224 | 1225 | ::: 1226 | 1227 | (the-docs-session)= 1228 | 1229 | ### The docs session 1230 | 1231 | Build the documentation using the Nox session `docs`: 1232 | 1233 | ```console 1234 | $ nox --session=docs 1235 | ``` 1236 | 1237 | The docs session runs the command `sphinx-autobuild` to generate the HTML documentation from the Sphinx directory. 1238 | This tool has several advantages over `sphinx-build` when you are editing the documentation files: 1239 | 1240 | - It rebuilds the documentation whenever a change is detected. 1241 | - It spins up a web server with live reloading. 1242 | - It opens the location of the web server in your browser. 1243 | 1244 | Use the `--` separator to pass additional options. 1245 | For example, to treat warnings as errors and run in nit-picky mode: 1246 | 1247 | ```console 1248 | $ nox --session=docs -- -W -n docs docs/_build 1249 | ``` 1250 | 1251 | This Nox session always runs with the current major release of Python. 1252 | 1253 | (the-docs-build-session)= 1254 | 1255 | ### The docs-build session 1256 | 1257 | The `docs-build` session runs the command `sphinx-build` to generate the HTML documentation from the Sphinx directory. 1258 | 1259 | This session is meant to be run as a part of automated checks. 1260 | Use the interactive `docs` session instead while you're editing the documentation. 1261 | 1262 | This Nox session always runs with the current major release of Python. 1263 | 1264 | (the-mypy-session)= 1265 | 1266 | ### The mypy session 1267 | 1268 | [mypy] is the pioneer and _de facto_ reference implementation of static type checking in Python. 1269 | Learn more about it in the section [Type-checking with mypy](type-checking-with-mypy). 1270 | 1271 | Run mypy using Nox: 1272 | 1273 | ```console 1274 | $ nox --session=mypy 1275 | ``` 1276 | 1277 | You can also run the type checker with a specific Python version. 1278 | For example, the following command runs mypy 1279 | using the current stable release of Python: 1280 | 1281 | ```console 1282 | $ nox --session=mypy --python=3.10 1283 | ``` 1284 | 1285 | Use the separator `--` to pass additional options and arguments to `mypy`. 1286 | For example, the following command type-checks only the `__main__` module: 1287 | 1288 | ```console 1289 | $ nox --session=mypy -- src//__main__.py 1290 | ``` 1291 | 1292 | (the-pre-commit-session)= 1293 | 1294 | ### The pre-commit session 1295 | 1296 | [pre-commit] is a multi-language linter framework and a Git hook manager. 1297 | Learn more about it in the section [Linting with pre-commit](linting-with-pre-commit). 1298 | 1299 | Run pre-commit from Nox using the `pre-commit` session: 1300 | 1301 | ```console 1302 | $ nox --session=pre-commit 1303 | ``` 1304 | 1305 | This session always runs with the current stable release of Python. 1306 | 1307 | Use the separator `--` to pass additional options to `pre-commit`. 1308 | For example, the following command installs the pre-commit hooks, 1309 | so they run automatically on every commit you make: 1310 | 1311 | ```console 1312 | $ nox --session=pre-commit -- install 1313 | ``` 1314 | 1315 | (the-safety-session)= 1316 | 1317 | ### The safety session 1318 | 1319 | [Safety] checks the dependencies of your project for known security vulnerabilities, 1320 | using a curated database of insecure Python packages. 1321 | The {{ HPC }} uses the [poetry export] command 1322 | to convert Poetry's lock file to a [requirements file], 1323 | for consumption by Safety. 1324 | 1325 | Run [Safety] using the `safety` session: 1326 | 1327 | ```console 1328 | $ nox --session=safety 1329 | ``` 1330 | 1331 | This session always runs with the current stable release of Python. 1332 | 1333 | (the-tests-session)= 1334 | 1335 | ### The tests session 1336 | 1337 | Tests are written using the [pytest] testing framework. 1338 | Learn more about it in the section [The test suite](the-test-suite). 1339 | 1340 | Run the test suite using the Nox session `tests`: 1341 | 1342 | ```console 1343 | $ nox --session=tests 1344 | ``` 1345 | 1346 | The tests session runs the test suite against the installed code. 1347 | More specifically, the session builds a wheel from your project and 1348 | installs it into the Nox environment, 1349 | with dependencies pinned as specified by Poetry's lock file. 1350 | 1351 | You can also run the test suite with a specific Python version. 1352 | For example, the following command runs the test suite 1353 | using the current stable release of Python: 1354 | 1355 | ```console 1356 | $ nox --session=tests --python=3.10 1357 | ``` 1358 | 1359 | Use the separator `--` to pass additional options to `pytest`. 1360 | For example, the following command runs only the test case `test_main_succeeds`: 1361 | 1362 | ```console 1363 | $ nox --session=tests -- -k test_main_succeeds 1364 | ``` 1365 | 1366 | The tests session also installs [pygments], a Python syntax highlighter. 1367 | It is used by pytest to highlight code in tracebacks, 1368 | improving the readability of test failures. 1369 | 1370 | (the-coverage-session)= 1371 | 1372 | ### The coverage session 1373 | 1374 | :::{note} 1375 | _Test coverage_ is a measure of the degree to which 1376 | the source code of your program is executed 1377 | while running its test suite. 1378 | ::: 1379 | 1380 | The coverage session prints a detailed coverage report to the terminal, 1381 | combining the coverage data collected 1382 | during the [tests session](the-tests-session). 1383 | If the total coverage is below 100%, 1384 | the coverage session fails. 1385 | Code coverage is measured using [Coverage.py]. 1386 | 1387 | The coverage session is triggered by the tests session, 1388 | and runs after all other sessions have completed. 1389 | This allows it to combine the coverage data for different Python versions. 1390 | 1391 | You can also run the session manually: 1392 | 1393 | ```console 1394 | $ nox --session=coverage 1395 | ``` 1396 | 1397 | Use the `--` separator to pass arguments to the `coverage` command. 1398 | For example, here's how you would generate an HTML report 1399 | in the `htmlcov` directory: 1400 | 1401 | ```console 1402 | $ nox -rs coverage -- html 1403 | ``` 1404 | 1405 | [Coverage.py] is configured in the `pyproject.toml` file, 1406 | using the `tool.coverage` table. 1407 | The configuration informs the tool about your package name and source tree layout. 1408 | It also enables branch analysis and the display of line numbers for missing coverage, 1409 | and specifies the target coverage percentage. 1410 | Coverage is measured for the package as well as [the test suite itself][batchelder include]. 1411 | 1412 | During continuous integration, 1413 | coverage data is uploaded to the [Codecov] reporting service. 1414 | For details, see the sections about 1415 | [Codecov](codecov-integration) and 1416 | [The Tests workflow](the-tests-workflow). 1417 | 1418 | (the-typeguard-session)= 1419 | 1420 | ### The typeguard session 1421 | 1422 | [Typeguard] is a runtime type checker and [pytest] plugin. 1423 | It can type-check function calls during test runs via an [import hook]. 1424 | 1425 | Typeguard checks that arguments passed to functions 1426 | match the type annotations of the function parameters, 1427 | and that the return value provided by the function 1428 | matches the return type annotation. 1429 | In the case of generator functions, 1430 | Typeguard checks the yields, sends and the return value 1431 | against the `Generator` annotation. 1432 | 1433 | Run [Typeguard] using Nox: 1434 | 1435 | ```console 1436 | $ nox --session=typeguard 1437 | ``` 1438 | 1439 | The typeguard session runs the test suite with runtime type-checking enabled. 1440 | It is similar to the [tests session](the-tests-session), 1441 | with the difference that your package is instrumented by Typeguard. 1442 | 1443 | This session always runs with the current stable release of Python. 1444 | 1445 | Use the separator `--` to pass additional options and arguments to pytest. 1446 | For example, the following command runs only tests for the `__main__` module: 1447 | 1448 | ```console 1449 | $ nox --session=typeguard -- tests/test_main.py 1450 | ``` 1451 | 1452 | :::{note} 1453 | Typeguard generates a warning about missing type annotations for a Click object. 1454 | This is due to the fact that `__main__.main` is wrapped by a decorator, 1455 | and its type annotations only apply to the inner function, 1456 | not the resulting object as seen by the test suite. 1457 | ::: 1458 | 1459 | (the-xdoctest-session)= 1460 | 1461 | ### The xdoctest session 1462 | 1463 | The [xdoctest] tool 1464 | runs examples in your docstrings and 1465 | compares the actual output to the expected output as per the docstring. 1466 | This serves multiple purposes: 1467 | 1468 | - The example is checked for correctness. 1469 | - You ensure that the documentation is up-to-date. 1470 | - Your codebase gets additional test coverage for free. 1471 | 1472 | Run the tool using the Nox session `xdoctest`: 1473 | 1474 | ```console 1475 | $ nox --session=xdoctest 1476 | ``` 1477 | 1478 | You can also run the test suite with a specific Python version. 1479 | For example, the following command runs the examples 1480 | using the current stable release of Python: 1481 | 1482 | ```console 1483 | $ nox --session=xdoctest --python=3.10 1484 | ``` 1485 | 1486 | By default, the Nox session uses the `all` subcommand to run all examples. 1487 | You can also list examples using the `list` subcommand, 1488 | or run specific examples: 1489 | 1490 | ```console 1491 | $ nox --session=xdoctest -- list 1492 | ``` 1493 | 1494 | (linting-with-pre-commit)= 1495 | 1496 | ## Linting with pre-commit 1497 | 1498 | [pre-commit] is a multi-language linter framework and a Git hook manager. 1499 | It allows you to 1500 | integrate linters and formatters into your Git workflow, 1501 | even when written in a language other than Python. 1502 | 1503 | pre-commit is configured using the file `.pre-commit-config.yaml` 1504 | in the project directory. 1505 | Please refer to the [official documentation][pre-commit configuration] 1506 | for details about the configuration file. 1507 | 1508 | ### Running pre-commit from Nox 1509 | 1510 | pre-commit runs in a Nox session every time you invoke `nox`: 1511 | 1512 | ```console 1513 | $ nox 1514 | ``` 1515 | 1516 | Run the pre-commit session explicitly like this: 1517 | 1518 | ```console 1519 | $ nox --session=pre-commit 1520 | ``` 1521 | 1522 | The session is described in more detail in the section [The pre-commit session](the-pre-commit-session). 1523 | 1524 | ### Running pre-commit from git 1525 | 1526 | When installed as a [Git hook], 1527 | pre-commit runs automatically every time you invoke `git commit`. 1528 | The commit is aborted if any check fails. 1529 | When invoked in this mode, pre-commit only runs on files staged for the commit. 1530 | 1531 | Install pre-commit as a Git hook by running the following command: 1532 | 1533 | ```console 1534 | $ nox --session=pre-commit -- install 1535 | ``` 1536 | 1537 | ### Managing hooks with pre-commit 1538 | 1539 | Hooks in languages other than Python, such as `prettier`, 1540 | run in isolated environments managed by pre-commit. 1541 | To upgrade these hooks, use the [autoupdate][pre-commit autoupdate] command: 1542 | 1543 | ```console 1544 | $ nox --session=pre-commit -- autoupdate 1545 | ``` 1546 | 1547 | ### Python-language hooks 1548 | 1549 | :::{note} 1550 | This section provides some background information about 1551 | how this project template integrates pre-commit with Poetry and Nox. 1552 | You can safely skip this section. 1553 | ::: 1554 | 1555 | Python-language hooks in the {{ HPC }} are not managed by pre-commit. 1556 | Instead, they are tracked as development dependencies in Poetry, 1557 | and installed into the Nox session alongside pre-commit itself. 1558 | As development dependencies, they are also present in the Poetry environment. 1559 | 1560 | This approach has some advantages: 1561 | 1562 | - All project dependencies are managed by Poetry. 1563 | - Hooks receive automatic upgrades from Dependabot. 1564 | - Nox can serve as a single entry point for all checks. 1565 | - Additional hook dependencies can be upgraded by a dependency manager. 1566 | An example for this are Flake8 extensions. 1567 | By contrast, `pre-commit autoupdate` does not include additional dependencies. 1568 | - Dependencies of dependencies (_subdependencies_) can be locked automatically, 1569 | making checks more repeatable and deterministic. 1570 | - Linters and formatters are available in the Poetry environment, 1571 | which is useful for editor integration. 1572 | 1573 | There are also some drawbacks to this technique: 1574 | 1575 | - This is not the officially supported way to integrate pre-commit hooks. 1576 | - The hook scripts installed by pre-commit do not activate the virtual environment 1577 | in which pre-commit and the hooks are installed. 1578 | To work around this limitation, 1579 | the Nox session patches hook scripts on installation. 1580 | - Adding a hook is more work, 1581 | including updating `pyproject.toml` and `noxfile.py`, and 1582 | adding the hook definition to `pre-commit-config.yaml`. 1583 | 1584 | You can always opt out of this integration method, 1585 | by removing the `repo: local` section from the configuration file, 1586 | and adding the official pre-commit hooks instead. 1587 | Don't forget to remove the hooks from Poetry's dependencies and from the Nox session. 1588 | 1589 | :::{note} 1590 | Python-language hooks in the {{ HPC }} are defined as [system hooks][pre-commit system hooks]. 1591 | System hooks don't have their environments managed by pre-commit; 1592 | instead, pre-commit assumes that hook dependencies have already been installed 1593 | and are available in its environment. 1594 | The Nox session for pre-commit takes care of 1595 | installing the Python hooks alongside pre-commit. 1596 | 1597 | Furthermore, the {{ HPC }} defines Python-language hooks as [repository-local hooks][pre-commit repository-local hooks]. 1598 | As such, hook definitions are not supplied by the hook repositories, 1599 | but by the project itself. 1600 | This makes it possible to override the hook language to `system`, as explained above. 1601 | ::: 1602 | 1603 | ### Adding an official pre-commit hook 1604 | 1605 | Adding the official pre-commit hook for a linter is straightforward. 1606 | Often you can simply copy a configuration snippet from the repository's `README`. 1607 | Otherwise, note the hook identifier from the `pre-commit-hooks.yaml` file, 1608 | and the git tag for the latest version. 1609 | Add the following section to your `pre-commit-config.yaml`, under `repos`: 1610 | 1611 | ```yaml 1612 | - repo: 1613 | rev: 1614 | hooks: 1615 | - id: 1616 | ``` 1617 | 1618 | While this technique also works for Python-language hooks, 1619 | it is recommended to integrate Python hooks with Nox and Poetry, 1620 | as shown in the next section. 1621 | 1622 | ### Adding a Python-language hook 1623 | 1624 | Adding a Python-language hook to your project takes three steps: 1625 | 1626 | - Add the hook as a Poetry development dependency. 1627 | - Install the hook in the Nox session for pre-commit. 1628 | - Add the hook to `pre-commit-config.yaml`. 1629 | 1630 | For example, consider a linter named `awesome-linter`. 1631 | 1632 | First, use Poetry to add the linter to your development dependencies: 1633 | 1634 | ```console 1635 | $ poetry add --dev awesome-linter 1636 | ``` 1637 | 1638 | Next, update `noxfile.py` to add the linter to the pre-commit session: 1639 | 1640 | ```python 1641 | @nox.session(name="pre-commit", ...) 1642 | def precommit(session: Session) -> None: 1643 | ... 1644 | session.install( 1645 | "awesome-linter", # Install awesome-linter 1646 | "black", 1647 | "darglint", 1648 | ... 1649 | ) 1650 | ``` 1651 | 1652 | Finally, add the hook to `pre-commit-config.yaml` as follows: 1653 | 1654 | - Locate the `pre-commit-hooks.yaml` file in the `awesome-linter` repository. 1655 | - Copy the entry for the hook (not just the hook identifier). 1656 | - Change `language:` from `python` to `system`. 1657 | - Add the hook definition to the `repo: local` section. 1658 | 1659 | Depending on the linter, the hook definition might look somewhat like the following: 1660 | 1661 | ```yaml 1662 | repos: 1663 | - repo: local 1664 | hooks: 1665 | # ... 1666 | - id: awesome-linter 1667 | name: Awesome Linter 1668 | entry: awesome-linter 1669 | language: system # was: python 1670 | types: [python] 1671 | ``` 1672 | 1673 | ### Running checks on modified files 1674 | 1675 | pre-commit runs checks on the _staged_ contents of files. 1676 | Any local modifications are stashed for the duration of the checks. 1677 | This is motivated by pre-commit's primary use case, 1678 | validating changes staged for a commit. 1679 | 1680 | Requiring changes to be staged allows for a nice property: 1681 | Many pre-commit hooks support fixing offending lines automatically, 1682 | for example `black`, `prettier`, and `isort`. 1683 | When this happens, 1684 | your original changes are in the staging area, 1685 | while the fixes are in the work tree. 1686 | You can accept the fixes by staging them with `git add` 1687 | before committing again. 1688 | 1689 | If you want to run linters or formatters on modified files, 1690 | and you do not want to stage the modifications just yet, 1691 | you can also invoke the tools via Poetry instead. 1692 | For example, use `poetry run flake8 ` to lint a modified file with Flake8. 1693 | 1694 | ### Overview of pre-commit hooks 1695 | 1696 | The {{ HPC }} comes with a pre-commit configuration consisting of the following hooks: 1697 | 1698 | :::{list-table} pre-commit hooks 1699 | :widths: auto 1700 | 1701 | - - [black] 1702 | - Run the [Black] code formatter 1703 | - - [flake8] 1704 | - Run the [Flake8] linter 1705 | - - [isort] 1706 | - Rewrite source code to sort Python imports 1707 | - - [prettier] 1708 | - Run the [Prettier] code formatter 1709 | - - [pyupgrade] 1710 | - Upgrade syntax to newer versions of Python 1711 | - - [check-added-large-files] 1712 | - Prevent giant files from being committed 1713 | - - [check-toml] 1714 | - Validate [TOML] files 1715 | - - [check-yaml] 1716 | - Validate [YAML] files 1717 | - - [end-of-file-fixer] 1718 | - Ensure files are terminated by a single newline 1719 | - - [trailing-whitespace] 1720 | - Ensure lines do not contain trailing whitespace 1721 | 1722 | ::: 1723 | 1724 | ### The Black hook 1725 | 1726 | [Black] is the uncompromising Python code formatter. 1727 | One of its greatest features is its lack of configurability. 1728 | Blackened code looks the same regardless of the project you're reading. 1729 | 1730 | ### The Prettier hook 1731 | 1732 | [Prettier] is an opinionated code formatter for many languages, 1733 | including YAML, Markdown, and JavaScript. 1734 | Like Black, it has few options, 1735 | and the {{ HPC }} uses none of them. 1736 | 1737 | (the-flake8-hook)= 1738 | 1739 | ### The Flake8 hook 1740 | 1741 | [Flake8] is an extensible linter framework for Python. 1742 | For more details, see the section [Linting with Flake8](linting-with-flake8). 1743 | 1744 | (the-isort-hook)= 1745 | 1746 | ### The isort hook 1747 | 1748 | [isort] reorders imports in your Python code. 1749 | Imports are separated into three sections, 1750 | as recommended by [PEP 8][pep 8]: standard library, third party, first party. 1751 | There are two additional sections, 1752 | one at the top for [future imports], 1753 | the other at the bottom for [relative imports]. 1754 | Within each section, `from` imports follow normal imports. 1755 | Imports are then sorted alphabetically. 1756 | 1757 | The {{ HPC }} activates the [Black profile][isort black profile] for compatibility with the Black code formatter. 1758 | Furthermore, the [force_single_line][isort force_single_line] setting is enabled. 1759 | This splits imports onto separate lines to avoid merge conflicts. 1760 | Finally, two blank lines are enforced after imports for consistency, 1761 | via the [lines_after_imports][isort lines_after_imports] setting. 1762 | 1763 | ### The pyupgrade hook 1764 | 1765 | [pyupgrade] upgrades your source code 1766 | to newer versions of the Python language and standard library. 1767 | The tool analyzes the [abstract syntax tree] of the modules in your project, 1768 | replacing deprecated or legacy usages with modern idioms. 1769 | 1770 | The minimum supported Python version is declared in the relevant section of `.pre-commit-config.yaml`. 1771 | You should change this setting whenever you drop support for an old version of Python. 1772 | 1773 | ### Hooks from pre-commit-hooks 1774 | 1775 | The pre-commit configuration also includes several smaller hooks 1776 | from the [pre-commit-hooks] repository. 1777 | 1778 | (linting-with-flake8)= 1779 | 1780 | ## Linting with Flake8 1781 | 1782 | [Flake8] is an extensible linter framework for Python, 1783 | and a command-line utility to run the linters on your source code. 1784 | The {{ HPC }} integrates Flake8 via a [pre-commit] hook, 1785 | see the section [The Flake8 hook](the-flake8-hook). 1786 | 1787 | The configuration file for Flake8 and its extensions 1788 | is named `.flake8` and located in the project directory. 1789 | For details about the configuration file, see the [official reference][flake8 configuration]. 1790 | 1791 | The sections below describe the linters in more detail. 1792 | Each section also notes any configuration settings applied by the {{ HPC }}. 1793 | 1794 | ### Overview of available plugins 1795 | 1796 | Flake8 comes with a rich ecosystem of plugins. 1797 | The following table lists the Flake8 plugins used by the {{ HPC }}, 1798 | and links to their lists of error codes. 1799 | 1800 | :::{list-table} Flake8 plugins 1801 | :widths: auto 1802 | 1803 | - - [pyflakes] 1804 | - Find invalid Python code 1805 | - [F][pyflakes codes] 1806 | - - [pycodestyle] 1807 | - Enforce style conventions from [PEP 8] 1808 | - [E,W][pycodestyle codes] 1809 | - - [pep8-naming] 1810 | - Enforce naming conventions from [PEP 8] 1811 | - [N][pep8-naming codes] 1812 | - - [pydocstyle] / [flake8-docstrings] 1813 | - Enforce docstring conventions from [PEP 257] 1814 | - [D][pydocstyle codes] 1815 | - - [flake8-rst-docstrings] 1816 | - Find invalid [reStructuredText] in docstrings 1817 | - [RST][flake8-rst-docstrings codes] 1818 | - - [flake8-bugbear] 1819 | - Detect bugs and design problems 1820 | - [B][flake8-bugbear codes] 1821 | - - [mccabe] 1822 | - Limit the code complexity 1823 | - [C][mccabe codes] 1824 | - - [darglint] 1825 | - Detect inaccurate docstrings 1826 | - [DAR][darglint codes] 1827 | - - [Bandit] / [flake8-bandit] 1828 | - Detect common security issues 1829 | - [S][bandit codes] 1830 | 1831 | ::: 1832 | 1833 | ### pyflakes 1834 | 1835 | [pyflakes] parses Python source files and finds invalid code. 1836 | Warnings reported by this tool include 1837 | syntax errors, 1838 | undefined names, 1839 | unused imports or variables, 1840 | and more. 1841 | It is included with [Flake8] by default. 1842 | 1843 | [Error codes][pyflakes codes] are prefixed by `F` for "flake". 1844 | 1845 | ### pycodestyle 1846 | 1847 | [pycodestyle] checks your code against the style recommendations of [PEP 8][pep 8], 1848 | the official Python style guide. 1849 | The tool detects 1850 | whitespace and indentation issues, 1851 | deprecated features, 1852 | bare excepts, 1853 | and much more. 1854 | It is included with [Flake8] by default. 1855 | 1856 | [Error codes][pycodestyle codes] are prefixed by `W` for warnings and `E` for errors. 1857 | 1858 | The {{ HPC }} disables the following errors and warnings 1859 | for compatibility with [Black] and [flake8-bugbear]: 1860 | 1861 | - `E203` (whitespace before `:`) 1862 | - `E501` (line too long) 1863 | - `W503` (line break before binary operator) 1864 | 1865 | ### pep8-naming 1866 | 1867 | [pep8-naming] enforces the naming conventions from [PEP 8][pep 8]. 1868 | Examples are the use of camel case for the names of classes, 1869 | the use of lowercase for the names of functions, arguments and variables, 1870 | or the convention to name the first argument of methods `self`. 1871 | 1872 | [Error codes][pep8-naming codes] are prefixed by `N` for "naming". 1873 | 1874 | ### pydocstyle and flake8-docstrings 1875 | 1876 | [pydocstyle] checks that docstrings comply with the recommendations of [PEP 257][pep 257] 1877 | and a configurable style convention. 1878 | It is integrated with Flake8 via the [flake8-docstrings] extension. 1879 | Warnings range from missing docstrings to 1880 | issues with whitespace, quoting, and docstring content. 1881 | 1882 | [Error codes][pydocstyle codes] are prefixed by `D` for "docstring". 1883 | 1884 | The {{ HPC }} selects the recommendations of the 1885 | [Google styleguide][google docstring style]. 1886 | Here is an example of a function documented in Google style: 1887 | 1888 | ```python 1889 | def add(first: int, second: int) -> int: 1890 | """Add two integers. 1891 | 1892 | Args: 1893 | first: The first argument. 1894 | second: The second argument. 1895 | 1896 | Returns: 1897 | The sum of the arguments. 1898 | """ 1899 | ``` 1900 | 1901 | ### flake8-rst-docstrings 1902 | 1903 | [flake8-rst-docstrings] validates docstring markup as [reStructuredText]. 1904 | Docstrings must be valid reStructuredText 1905 | because they are used by Sphinx to generate the API reference. 1906 | 1907 | [Error codes][flake8-rst-docstrings codes] are prefixed by `RST` for "reStructuredText", 1908 | and group issues into numerical blocks, by their severity and origin. 1909 | 1910 | ### flake8-bugbear 1911 | 1912 | [flake8-bugbear] detects bugs and design problems. 1913 | The warnings are more opinionated than those of pyflakes or pycodestyle. 1914 | For example, 1915 | the plugin detects Python 2 constructs which have been removed in Python 3, 1916 | and likely bugs such as function arguments defaulting to empty lists or dictionaries. 1917 | 1918 | [Error codes][flake8-bugbear codes] are prefixed by `B` for "bugbear". 1919 | 1920 | The {{ HPC }} also enables Bugbear's `B9` warnings, 1921 | which are disabled by default. 1922 | In particular, `B950` checks the maximum line length 1923 | like [pycodestyle]'s `E501`, 1924 | but with a tolerance margin of 10%. 1925 | This soft limit is set to 80 characters, 1926 | which is the value used by the Black code formatter. 1927 | 1928 | ### mccabe 1929 | 1930 | [mccabe] checks the [code complexity][cyclomatic complexity] 1931 | of your Python package against a configured limit. 1932 | The tool is included with [Flake8]. 1933 | 1934 | [Error codes][mccabe codes] are prefixed by `C` for "complexity". 1935 | 1936 | The {{ HPC }} limits code complexity to a value of 10. 1937 | 1938 | (darglint-integration)= 1939 | 1940 | ### darglint 1941 | 1942 | [darglint] checks that docstring descriptions match function definitions. 1943 | The tool has its own configuration file, named `.darglint`. 1944 | 1945 | [Error codes][darglint codes] are prefixed by `DAR` for "darglint". 1946 | 1947 | The {{ HPC }} allows one-line docstrings without function signatures. 1948 | Multi-line docstrings must 1949 | specify the function signatures completely and correctly, 1950 | using [Google docstring style]. 1951 | 1952 | ### Bandit 1953 | 1954 | [Bandit] is a tool designed to 1955 | find common security issues in Python code, 1956 | and integrated via the [flake8-bandit] extension. 1957 | 1958 | [Error codes][bandit codes] are prefixed by `S` for "security". 1959 | (The prefix `B` for "bandit" is used 1960 | when Bandit is run as a stand-alone tool.) 1961 | 1962 | The {{ HPC }} disables `S101` (use of assert) for the test suite, 1963 | as [pytest] uses assertions to verify expectations in tests. 1964 | 1965 | (type-checking-with-mypy)= 1966 | 1967 | ## Type-checking with mypy 1968 | 1969 | :::{note} 1970 | [Type annotations], first introduced in Python 3.5, 1971 | are a way to annotate functions and variables with types. 1972 | With appropriate tooling, 1973 | they can make your programs easier to understand, debug, and maintain. 1974 | 1975 | _Type-checking_ refers to the practice of 1976 | verifying the type correctness of a program, 1977 | using type annotations and type inference. 1978 | There are two kinds of type checkers: 1979 | 1980 | - _Static type checkers_ verify the type correctness of your program 1981 | without executing it, using static analysis. 1982 | - _Runtime type checkers_ find type errors by instrumenting your code to 1983 | type-check arguments and return values in function calls. 1984 | This is particularly useful during the execution of unit tests. 1985 | 1986 | There is also an increasing number of libraries 1987 | that leverage type annotations at runtime. 1988 | For example, you can use type annotations to generate serialization schemas 1989 | or command-line parsers. 1990 | ::: 1991 | 1992 | [mypy] is the pioneer and _de facto_ reference implementation of static type checking in Python. 1993 | Invoke mypy via Nox, as explained in the section [The mypy session](the-mypy-session). 1994 | 1995 | mypy is configured in the `pyproject.toml` file, 1996 | using the `tool.mypy` table. For details about supported configuration 1997 | options, see the [official reference][mypy configuration]. 1998 | 1999 | The {{ HPC }} enables several configuration options which are off by default. 2000 | The following options are enabled for strictness and enhanced output: 2001 | 2002 | - {option}`strict ` 2003 | - {option}`warn_unreachable ` 2004 | - {option}`pretty ` 2005 | - {option}`show_column_numbers ` 2006 | - {option}`show_error_codes ` 2007 | - {option}`show_error_context ` 2008 | 2009 | (external-services)= 2010 | 2011 | ## External services 2012 | 2013 | Your GitHub repository can be integrated with several external services 2014 | for continuous integration and delivery. 2015 | This section describes these external services, 2016 | what they do, and how to set them up for your repository. 2017 | 2018 | ### PyPI 2019 | 2020 | [PyPI] is the official Python Package Index. 2021 | Uploading your package to PyPI allows others to 2022 | download and install it to their system. 2023 | 2024 | Follow these steps to set up PyPI for your repository: 2025 | 2026 | 1. Sign up at [PyPI]. 2027 | 2. Go to the Account Settings on PyPI, 2028 | generate an API token, and copy it. 2029 | 3. Go to the repository settings on GitHub, and 2030 | add a secret named `PYPI_TOKEN` with the token you just copied. 2031 | 2032 | PyPI is integrated with your repository 2033 | via the [Release workflow](the-release-workflow). 2034 | 2035 | ### TestPyPI 2036 | 2037 | [TestPyPI] is a test instance of the Python package registry. 2038 | It allows you to check your release before uploading it to the real index. 2039 | 2040 | Follow these steps to set up TestPyPI for your repository: 2041 | 2042 | 1. Sign up at [TestPyPI]. 2043 | 2. Go to the Account Settings on TestPyPI, 2044 | generate an API token, and copy it. 2045 | 3. Go to the repository settings on GitHub, and 2046 | add a secret named `TEST_PYPI_TOKEN` with the token you just copied. 2047 | 2048 | TestPyPI is integrated with your repository 2049 | via the [Release workflow](the-release-workflow). 2050 | 2051 | (codecov-integration)= 2052 | 2053 | ### Codecov 2054 | 2055 | [Codecov] is a reporting service for code coverage. 2056 | 2057 | Follow these steps to set up Codecov for your repository: 2058 | 2059 | 1. Sign up at [Codecov]. 2060 | 2. Install their GitHub app. 2061 | 2062 | The configuration is included in the repository, 2063 | in the file [codecov.yml][codecov configuration]. 2064 | 2065 | Codecov integrates with your repository 2066 | via its GitHub app. 2067 | The [Tests workflow](the-tests-workflow) uploads the coverage data. 2068 | 2069 | (dependabot-integration)= 2070 | 2071 | ### Dependabot 2072 | 2073 | [Dependabot] creates pull requests with automated dependency updates. 2074 | 2075 | Please refer to the [official documentation][dependabot docs] for more details. 2076 | 2077 | The configuration is included in the repository, in the file [.github/dependabot.yml]. 2078 | 2079 | It manages the following dependencies: 2080 | 2081 | :::{list-table} 2082 | :header-rows: 1 2083 | :widths: auto 2084 | 2085 | - - Type of dependency 2086 | - Managed files 2087 | - See also 2088 | - - Python 2089 | - `poetry.lock` 2090 | - [Managing dependencies](managing-dependencies) 2091 | - - Python 2092 | - `docs/requirements.txt` 2093 | - [Read the Docs](read-the-docs-integration) 2094 | - - Python 2095 | - `.github/workflows/constraints.txt` 2096 | - [Constraints file](workflow-constraints) 2097 | - - GitHub Action 2098 | - `.github/workflows/*.yml` 2099 | - [GitHub Actions workflows](github-actions-workflows) 2100 | 2101 | ::: 2102 | 2103 | (read-the-docs-integration)= 2104 | 2105 | ### Read the Docs 2106 | 2107 | [Read the Docs] automates the building, versioning, and hosting of documentation. 2108 | 2109 | Follow these steps to set up Read the Docs for your repository: 2110 | 2111 | 1. Sign up at [Read the Docs]. 2112 | 2. Import your GitHub repository, 2113 | using the button _Import a Project_. 2114 | 3. Install the GitHub [webhook][readthedocs webhooks], 2115 | using the button _Add integration_ 2116 | on the _Integrations_ tab 2117 | in the _Admin_ section of your project 2118 | on Read the Docs. 2119 | 2120 | Read the Docs automatically starts building your documentation, 2121 | and will continue to do so when you push to the default branch or make a release. 2122 | Your documentation now has a public URL like this: 2123 | 2124 | > _https://\.readthedocs.io/_ 2125 | 2126 | The configuration for Read the Docs is included in the repository, 2127 | in the file [.readthedocs.yml]. 2128 | The {{ HPC }} configures Read the Docs 2129 | to build and install the package with Poetry, 2130 | using a so-called [PEP 517][pep 517]-build. 2131 | 2132 | Build dependencies for the documentation 2133 | are installed using a [requirements file] located at `docs/requirements.txt`. 2134 | Read the Docs currently does not support 2135 | installing development dependencies using Poetry's lock file. 2136 | For the sake of brevity and maintainability, 2137 | only direct dependencies are included. 2138 | 2139 | :::{note} 2140 | The requirements file is managed by [Dependabot](dependabot-integration). 2141 | When newer versions of the build dependencies become available, 2142 | Dependabot updates the requirements file and submits a pull request. 2143 | When adding or removing Sphinx extensions using Poetry, 2144 | don't forget to update the requirements file as well. 2145 | ::: 2146 | 2147 | (github-actions-workflows)= 2148 | 2149 | ## GitHub Actions workflows 2150 | 2151 | The {{ HPC }} uses [GitHub Actions] 2152 | to implement continuous integration and delivery. 2153 | With GitHub Actions, 2154 | you define so-called workflows 2155 | using [YAML] files located in the `.github/workflows` directory. 2156 | 2157 | A _workflow_ is an automated process 2158 | consisting of one or many jobs, 2159 | each of which executes a series of steps. 2160 | Workflows are triggered by events, 2161 | for example when a commit is pushed 2162 | or when a release is published. 2163 | You can learn more about 2164 | the workflow language and its supported keywords 2165 | in the [official reference][github actions syntax]. 2166 | 2167 | :::{note} 2168 | Real-time logs for workflow runs are available 2169 | from the _Actions_ tab in your GitHub repository. 2170 | ::: 2171 | 2172 | ### Overview of workflows 2173 | 2174 | The {{ HPC }} defines the following workflows: 2175 | 2176 | :::{list-table} GitHub Actions workflows 2177 | :header-rows: 1 2178 | :widths: auto 2179 | 2180 | - - Workflow 2181 | - File 2182 | - Description 2183 | - Trigger 2184 | - - [Tests](the-tests-workflow) 2185 | - `tests.yml` 2186 | - Run the test suite with [Nox] 2187 | - Push, PR 2188 | - - [Release](the-release-workflow) 2189 | - `release.yml` 2190 | - Upload the package to [PyPI] 2191 | - Push (default branch) 2192 | - - [Labeler](the-labeler-workflow) 2193 | - `labeler.yml` 2194 | - Manage GitHub project labels 2195 | - Push (default branch) 2196 | 2197 | ::: 2198 | 2199 | ### Overview of GitHub Actions 2200 | 2201 | Workflows use the following GitHub Actions: 2202 | 2203 | :::{list-table} GitHub Actions 2204 | :widths: auto 2205 | 2206 | - - [actions/cache] 2207 | - Cache dependencies and build outputs 2208 | - - [actions/checkout] 2209 | - Check out the Git repository 2210 | - - [actions/download-artifact] 2211 | - Download artifacts from workflows 2212 | - - [actions/setup-python] 2213 | - Set up workflows with a specific Python version 2214 | - - [actions/upload-artifact] 2215 | - Upload artifacts from workflows 2216 | - - [codecov/codecov-action] 2217 | - Upload coverage to Codecov 2218 | - - [crazy-max/ghaction-github-labeler] 2219 | - Manage labels on GitHub as code 2220 | - - [pypa/gh-action-pypi-publish] 2221 | - Upload packages to PyPI and TestPyPI 2222 | - - [release-drafter/release-drafter] 2223 | - Draft and publish GitHub Releases 2224 | - - [salsify/action-detect-and-tag-new-version] 2225 | - Detect and tag new versions in a repository 2226 | 2227 | ::: 2228 | 2229 | :::{note} 2230 | GitHub Actions used by the workflows are managed by [Dependabot](dependabot-integration). 2231 | When newer versions of GitHub Actions become available, 2232 | Dependabot updates the workflows that use them and submits a pull request. 2233 | ::: 2234 | 2235 | (workflow-constraints)= 2236 | 2237 | ### Constraints file 2238 | 2239 | GitHub Actions workflows install the following tools: 2240 | 2241 | - [pip] 2242 | - [virtualenv] 2243 | - [Poetry] 2244 | - [Nox] 2245 | 2246 | These dependencies are pinned using a [constraints file] 2247 | located in `.github/workflow/constraints.txt`. 2248 | 2249 | :::{note} 2250 | The constraints file is managed by [Dependabot](dependabot-integration). 2251 | When newer versions of the tools become available, 2252 | Dependabot updates the constraints file and submits a pull request. 2253 | ::: 2254 | 2255 | (the-tests-workflow)= 2256 | 2257 | ### The Tests workflow 2258 | 2259 | The Tests workflow runs checks using Nox. 2260 | It is triggered on every push to the repository, 2261 | and when a pull request is opened or receives new commits. 2262 | 2263 | Each Nox session runs in a separate job, 2264 | using the current release of Python 2265 | and the [latest Ubuntu runner][github actions runners]. 2266 | Selected Nox sessions also run on Windows and macOS, 2267 | and with older Python versions, 2268 | as shown in the table below: 2269 | 2270 | :::{list-table} Jobs in the Tests workflow 2271 | :widths: auto 2272 | 2273 | - - Nox session 2274 | - Platform 2275 | - Python versions 2276 | - - [pre-commit](the-pre-commit-session) 2277 | - Ubuntu 2278 | - 3.10 2279 | - - [safety](the-safety-session) 2280 | - Ubuntu 2281 | - 3.10 2282 | - - [mypy](the-mypy-session) 2283 | - Ubuntu 2284 | - 3.10, 3.9, 3.8, 3.7 2285 | - - [tests](the-tests-session) 2286 | - Ubuntu 2287 | - 3.10, 3.9, 3.8, 3.7 2288 | - - [tests](the-tests-session) 2289 | - Windows 2290 | - 3.10 2291 | - - [tests](the-tests-session) 2292 | - macOS 2293 | - 3.10 2294 | - - [coverage](the-coverage-session) 2295 | - Ubuntu 2296 | - 3.10 2297 | - - [docs-build](the-docs-build-session) 2298 | - Ubuntu 2299 | - 3.10 2300 | 2301 | ::: 2302 | 2303 | The workflow uploads the generated documentation as a [workflow artifact][github actions artifacts]. 2304 | Building the documentation only serves the purpose of catching issues in pull requests. 2305 | Builds on [Read the Docs] happen independently. 2306 | 2307 | The workflow also uploads coverage data to [Codecov] after running tests. 2308 | It generates a coverage report in [Cobertura] XML format, 2309 | using the [coverage session](the-coverage-session). 2310 | The report is uploaded 2311 | using the official [Codecov GitHub Action][codecov/codecov-action]. 2312 | 2313 | The Tests workflow uses the following GitHub Actions: 2314 | 2315 | - [actions/checkout] for checking out the Git repository 2316 | - [actions/setup-python] for setting up the Python interpreter 2317 | - [actions/download-artifact] to download the coverage data of each tests session 2318 | - [actions/cache] for caching pre-commit environments 2319 | - [actions/upload-artifact] to upload the generated documentation and the coverage data of each tests session 2320 | - [codecov/codecov-action] for uploading to [Codecov] 2321 | 2322 | The Tests workflow is defined in `.github/workflows/tests.yml`. 2323 | 2324 | (the-release-workflow)= 2325 | 2326 | ### The Release workflow 2327 | 2328 | The Release workflow publishes your package on [PyPI], the Python Package Index. 2329 | The workflow also creates a version tag in the GitHub repository, 2330 | and publishes a GitHub Release using [Release Drafter]. 2331 | The workflow is triggered on every push to the default branch. 2332 | 2333 | Release steps only run if the package version was bumped. 2334 | If the package version did not change, 2335 | the package is instead uploaded to [TestPyPI] as a prerelease, 2336 | and only a draft GitHub Release is created. 2337 | TestPyPI is a test instance of the Python Package Index. 2338 | 2339 | The Release workflow uses API tokens to access [PyPI] and [TestPyPI]. 2340 | You can generate these tokens from your account settings on these services. 2341 | The tokens need to be stored as secrets in the repository settings on GitHub: 2342 | 2343 | :::{list-table} Secrets 2344 | :widths: auto 2345 | 2346 | - - `PYPI_TOKEN` 2347 | - [PyPI] API token 2348 | - - `TEST_PYPI_TOKEN` 2349 | - [TestPyPI] API token 2350 | 2351 | ::: 2352 | 2353 | The Release workflow uses the following GitHub Actions: 2354 | 2355 | - [actions/checkout] for checking out the Git repository 2356 | - [actions/setup-python] for setting up the Python interpreter 2357 | - [salsify/action-detect-and-tag-new-version] for tagging on version bumps 2358 | - [pypa/gh-action-pypi-publish] for uploading the package to PyPI or TestPyPI 2359 | - [release-drafter/release-drafter] for publishing the GitHub Release 2360 | 2361 | Release notes are populated with the titles and authors of merged pull requests. 2362 | You can group the pull requests into separate sections 2363 | by applying labels to them, like this: 2364 | 2365 | ```{eval-rst} 2366 | .. include:: quickstart.md 2367 | :parser: myst_parser.sphinx_ 2368 | :start-after: 2369 | :end-before: 2370 | ``` 2371 | 2372 | The workflow is defined in `.github/workflows/release.yml`. 2373 | The Release Drafter configuration is located in `.github/release-drafter.yml`. 2374 | 2375 | (the-labeler-workflow)= 2376 | 2377 | ### The Labeler workflow 2378 | 2379 | The Labeler workflow manages the labels used in GitHub issues 2380 | and pull requests based on a description file `.github/labels.yaml`. 2381 | In this file each label is described with 2382 | a `name`, 2383 | a `description` 2384 | and a `color`. 2385 | The workflow is triggered on every push to the default branch. 2386 | 2387 | The workflow creates or updates project labels if they are missing 2388 | or different compared to the `labels.yml` file content. 2389 | 2390 | The workflow does not delete labels already configured in the GitHub UI 2391 | and not in the `labels.yml` file. 2392 | You can change this behavior and add ignore patterns 2393 | in the settings of the workflow (see [GitHub Labeler] documentation). 2394 | 2395 | The Labeler workflow uses the following GitHub Actions: 2396 | 2397 | - [actions/checkout] for checking out the Git repository 2398 | - [crazy-max/ghaction-github-labeler] for updating the GitHub project labels 2399 | 2400 | The workflow is defined in `.github/workflows/labeler.yml`. 2401 | The GitHub Labeler configuration is located in `.github/labels.yml`. 2402 | 2403 | (tutorials)= 2404 | 2405 | ## Tutorials 2406 | 2407 | First, make sure you have all the [requirements](installation) installed. 2408 | 2409 | (how-to-test-your-project)= 2410 | 2411 | ### How to test your project 2412 | 2413 | Run the test suite using [Nox](using-Nox): 2414 | 2415 | ```console 2416 | $ nox -r 2417 | ``` 2418 | 2419 | ### How to run your code 2420 | 2421 | First, install the project and its dependencies to the Poetry environment: 2422 | 2423 | ```console 2424 | $ poetry install 2425 | ``` 2426 | 2427 | Run an interactive session in the environment: 2428 | 2429 | ```console 2430 | $ poetry run python 2431 | ``` 2432 | 2433 | Invoke the command-line interface of your package: 2434 | 2435 | ```console 2436 | $ poetry run 2437 | ``` 2438 | 2439 | ### How to make code changes 2440 | 2441 | 1. Run the tests, 2442 | [as explained above](how-to-test-your-project).
2443 | All tests should pass. 2444 | 2. Add a failing test 2445 | [under the tests directory](the-test-suite).
2446 | Run the tests again to verify that your test fails. 2447 | 3. Make your changes to the package, 2448 | [under the src directory](the-initial-package).
2449 | Run the tests to verify that all tests pass again. 2450 | 2451 | ### How to push code changes 2452 | 2453 | Create a branch for your changes: 2454 | 2455 | ```console 2456 | $ git switch --create my-topic-branch main 2457 | ``` 2458 | 2459 | Create a series of small, single-purpose commits: 2460 | 2461 | ```console 2462 | $ git add 2463 | $ git commit 2464 | ``` 2465 | 2466 | Push your branch to GitHub: 2467 | 2468 | ```console 2469 | $ git push --set-upstream origin my-topic-branch 2470 | ``` 2471 | 2472 | The push triggers the following automated steps: 2473 | 2474 | - [The test suite runs against your branch](the-tests-workflow). 2475 | 2476 | ### How to open a pull request 2477 | 2478 | Open a pull request for your branch on GitHub: 2479 | 2480 | 1. Select your branch from the _Branch_ menu. 2481 | 2. Click **New pull request**. 2482 | 3. Enter the title for the pull request. 2483 | 4. Enter a description for the pull request. 2484 | 5. Apply a [label identifying the type of change](the-release-workflow) 2485 | 6. Click **Create pull request**. 2486 | 2487 | Release notes are pre-filled with the titles of merged pull requests. 2488 | 2489 | ### How to accept a pull request 2490 | 2491 | If all checks are marked as passed, 2492 | merge the pull request using the squash-merge strategy (recommended): 2493 | 2494 | 1. Click **Squash and Merge**. 2495 | (Select this option from the dropdown menu of the merge button, if it is not shown.) 2496 | 2. Click **Confirm squash and merge**. 2497 | 3. Click **Delete branch**. 2498 | 2499 | This triggers the following automated steps: 2500 | 2501 | - [The test suite runs against the main branch](the-tests-workflow). 2502 | - [The draft GitHub Release is updated](the-release-workflow). 2503 | - [A pre-release of the package is uploaded to TestPyPI](the-release-workflow). 2504 | - [Read the Docs] rebuilds the _latest_ version of the documentation. 2505 | 2506 | In your local repository, 2507 | update the main branch: 2508 | 2509 | ```console 2510 | $ git switch main 2511 | $ git pull origin main 2512 | ``` 2513 | 2514 | Optionally, remove the merged topic branch 2515 | from the local repository as well: 2516 | 2517 | ```console 2518 | $ git remote prune origin 2519 | $ git branch --delete --force my-topic-branch 2520 | ``` 2521 | 2522 | The original commits remain accessible from the pull request 2523 | (_Commits_ tab). 2524 | 2525 | ### How to make a release 2526 | 2527 | Releases are triggered by a version bump on the default branch. 2528 | It is recommended to do this in a separate pull request: 2529 | 2530 | 1. Switch to a branch. 2531 | 2. Bump the version using [poetry version]. 2532 | 3. Commit and push to GitHub. 2533 | 4. Open a pull request. 2534 | 5. Merge the pull request. 2535 | 2536 | The individual steps for bumping the version are: 2537 | 2538 | ```console 2539 | $ git switch --create release main 2540 | $ poetry version 2541 | $ git commit --message=" " pyproject.toml 2542 | $ git push origin release 2543 | ``` 2544 | 2545 | If you're not sure which version number to choose, 2546 | read about [Semantic Versioning]. 2547 | Versioning rules for Python packages are laid down in [PEP 440][pep 440]. 2548 | 2549 | Before merging the pull request for the release, 2550 | go through the following checklist: 2551 | 2552 | - The pull request passes all checks. 2553 | - The development release on [TestPyPI] looks good. 2554 | - All pull requests for the release have been merged. 2555 | 2556 | Merging the pull request triggers the 2557 | [Release workflow](the-release-workflow). 2558 | This workflow performs the following automated steps: 2559 | 2560 | - Publish the package on PyPI. 2561 | - Publish a GitHub Release. 2562 | - Apply a Git tag to the repository. 2563 | 2564 | [Read the Docs] automatically builds a new stable version of the documentation. 2565 | 2566 | ## The Hypermodern Python blog 2567 | 2568 | The project setup is described in detail in the [Hypermodern Python] article series: 2569 | 2570 | - [Chapter 1: Setup][hypermodern python chapter 1] 2571 | - [Chapter 2: Testing][hypermodern python chapter 2] 2572 | - [Chapter 3: Linting][hypermodern python chapter 3] 2573 | - [Chapter 4: Typing][hypermodern python chapter 4] 2574 | - [Chapter 5: Documentation][hypermodern python chapter 5] 2575 | - [Chapter 6: CI/CD][hypermodern python chapter 6] 2576 | 2577 | You can also read the articles on [this blog][hypermodern python blog]. 2578 | 2579 | [--reuse-existing-virtualenvs]: https://nox.thea.codes/en/stable/usage.html#re-using-virtualenvs 2580 | [.gitattributes]: https://git-scm.com/book/en/Customizing-Git-Git-Attributes 2581 | [.github/dependabot.yml]: https://docs.github.com/en/github/administering-a-repository/configuration-options-for-dependency-updates 2582 | [.gitignore]: https://git-scm.com/book/en/v2/Git-Basics-Recording-Changes-to-the-Repository#_ignoring 2583 | [.readthedocs.yml]: https://docs.readthedocs.io/en/stable/config-file/v2.html 2584 | [2022.6.3]: https://github.com/cjolowicz/cookiecutter-hypermodern-python/releases/tag/2022.6.3 2585 | [__main__]: https://docs.python.org/3/library/__main__.html 2586 | [abstract syntax tree]: https://docs.python.org/3/library/ast.html 2587 | [actions/cache]: https://github.com/actions/cache 2588 | [actions/checkout]: https://github.com/actions/checkout 2589 | [actions/download-artifact]: https://github.com/actions/download-artifact 2590 | [actions/setup-python]: https://github.com/actions/setup-python 2591 | [actions/upload-artifact]: https://github.com/actions/upload-artifact 2592 | [autodoc]: https://www.sphinx-doc.org/en/master/usage/extensions/autodoc.html 2593 | [bandit codes]: https://bandit.readthedocs.io/en/latest/plugins/index.html#complete-test-plugin-listing 2594 | [bandit]: https://github.com/PyCQA/bandit 2595 | [bash]: https://www.gnu.org/software/bash/ 2596 | [batchelder include]: https://nedbatchelder.com/blog/202008/you_should_include_your_tests_in_coverage.html 2597 | [black]: https://github.com/psf/black 2598 | [calendar versioning]: https://calver.org 2599 | [cannon semver]: https://snarky.ca/why-i-dont-like-semver/ 2600 | [check-added-large-files]: https://github.com/pre-commit/pre-commit-hooks#check-added-large-files 2601 | [check-toml]: https://github.com/pre-commit/pre-commit-hooks#check-toml 2602 | [check-yaml]: https://github.com/pre-commit/pre-commit-hooks#check-yaml 2603 | [click.testing.clirunner]: https://click.palletsprojects.com/en/7.x/testing/ 2604 | [click]: https://click.palletsprojects.com/ 2605 | [cobertura]: https://cobertura.github.io/cobertura/ 2606 | [codecov configuration]: https://docs.codecov.io/docs/codecov-yaml 2607 | [codecov/codecov-action]: https://github.com/codecov/codecov-action 2608 | [codecov]: https://codecov.io/ 2609 | [constraints file]: https://pip.pypa.io/en/stable/user_guide/#constraints-files 2610 | [contributor covenant]: https://www.contributor-covenant.org 2611 | [cookiecutter]: https://github.com/audreyr/cookiecutter 2612 | [coverage.py]: https://coverage.readthedocs.io/ 2613 | [crazy-max/ghaction-github-labeler]: https://github.com/crazy-max/ghaction-github-labeler 2614 | [cupper]: https://github.com/senseyeio/cupper 2615 | [curl]: https://curl.haxx.se 2616 | [cyclomatic complexity]: https://en.wikipedia.org/wiki/Cyclomatic_complexity 2617 | [darglint codes]: https://github.com/terrencepreilly/darglint#error-codes 2618 | [darglint]: https://github.com/terrencepreilly/darglint 2619 | [dependabot docs]: https://docs.github.com/en/github/administering-a-repository/keeping-your-dependencies-updated-automatically 2620 | [dependabot issue 4435]: https://github.com/dependabot/dependabot-core/issues/4435 2621 | [dependabot]: https://dependabot.com/ 2622 | [dev-prod parity]: https://12factor.net/dev-prod-parity 2623 | [editable install]: https://pip.pypa.io/en/stable/cli/pip_install/#install-editable 2624 | [end-of-file-fixer]: https://github.com/pre-commit/pre-commit-hooks#end-of-file-fixer 2625 | [flake8 configuration]: https://flake8.pycqa.org/en/latest/user/configuration.html 2626 | [flake8-bandit]: https://github.com/tylerwince/flake8-bandit 2627 | [flake8-bugbear codes]: https://github.com/PyCQA/flake8-bugbear#list-of-warnings 2628 | [flake8-bugbear]: https://github.com/PyCQA/flake8-bugbear 2629 | [flake8-docstrings]: https://gitlab.com/pycqa/flake8-docstrings 2630 | [flake8-rst-docstrings codes]: https://github.com/peterjc/flake8-rst-docstrings#flake8-validation-codes 2631 | [flake8-rst-docstrings]: https://github.com/peterjc/flake8-rst-docstrings 2632 | [flake8]: http://flake8.pycqa.org 2633 | [furo]: https://pradyunsg.me/furo/ 2634 | [future imports]: https://docs.python.org/3/library/__future__.html 2635 | [gabor version]: https://bernat.tech/posts/version-numbers/ 2636 | [git hook]: https://git-scm.com/book/en/v2/Customizing-Git-Git-Hooks 2637 | [git]: https://www.git-scm.com 2638 | [github actions artifacts]: https://help.github.com/en/actions/configuring-and-managing-workflows/persisting-workflow-data-using-artifacts 2639 | [github actions runners]: https://help.github.com/en/actions/automating-your-workflow-with-github-actions/virtual-environments-for-github-hosted-runners#supported-runners-and-hardware-resources 2640 | [github actions syntax]: https://help.github.com/en/actions/automating-your-workflow-with-github-actions/workflow-syntax-for-github-actions 2641 | [github actions]: https://github.com/features/actions 2642 | [github labeler]: https://github.com/marketplace/actions/github-labeler 2643 | [github release]: https://help.github.com/en/github/administering-a-repository/about-releases 2644 | [github renaming]: https://github.com/github/renaming 2645 | [github]: https://github.com/ 2646 | [google docstring style]: https://google.github.io/styleguide/pyguide.html#38-comments-and-docstrings 2647 | [hypermodern python blog]: https://cjolowicz.github.io/posts/hypermodern-python-01-setup/ 2648 | [hypermodern python chapter 1]: https://medium.com/@cjolowicz/hypermodern-python-d44485d9d769 2649 | [hypermodern python chapter 2]: https://medium.com/@cjolowicz/hypermodern-python-2-testing-ae907a920260 2650 | [hypermodern python chapter 3]: https://medium.com/@cjolowicz/hypermodern-python-3-linting-e2f15708da80 2651 | [hypermodern python chapter 4]: https://medium.com/@cjolowicz/hypermodern-python-4-typing-31bcf12314ff 2652 | [hypermodern python chapter 5]: https://medium.com/@cjolowicz/hypermodern-python-5-documentation-13219991028c 2653 | [hypermodern python chapter 6]: https://medium.com/@cjolowicz/hypermodern-python-6-ci-cd-b233accfa2f6 2654 | [hypermodern python cookiecutter]: https://github.com/cjolowicz/cookiecutter-hypermodern-python 2655 | [hypermodern python]: https://medium.com/@cjolowicz/hypermodern-python-d44485d9d769 2656 | [import hook]: https://docs.python.org/3/reference/import.html#import-hooks 2657 | [install-poetry.py]: https://raw.githubusercontent.com/python-poetry/poetry/master/install-poetry.py 2658 | [isort black profile]: https://pycqa.github.io/isort/docs/configuration/black_compatibility.html 2659 | [isort force_single_line]: https://pycqa.github.io/isort/docs/configuration/options.html#force-single-line 2660 | [isort lines_after_imports]: https://pycqa.github.io/isort/docs/configuration/options.html#lines-after-imports 2661 | [isort]: https://pycqa.github.io/isort/ 2662 | [jinja]: https://palletsprojects.com/p/jinja/ 2663 | [json]: https://www.json.org/ 2664 | [markdown]: https://spec.commonmark.org/current/ 2665 | [mccabe codes]: https://github.com/PyCQA/mccabe#plugin-for-flake8 2666 | [mccabe]: https://github.com/PyCQA/mccabe 2667 | [mit license]: https://opensource.org/licenses/MIT 2668 | [mypy configuration]: https://mypy.readthedocs.io/en/stable/config_file.html 2669 | [mypy]: http://mypy-lang.org/ 2670 | [myst]: https://myst-parser.readthedocs.io/ 2671 | [napoleon]: https://www.sphinx-doc.org/en/master/usage/extensions/napoleon.html 2672 | [nox-poetry]: https://nox-poetry.readthedocs.io/ 2673 | [nox]: https://nox.thea.codes/ 2674 | [package metadata]: https://packaging.python.org/en/latest/specifications/core-metadata/ 2675 | [pep 257]: http://www.python.org/dev/peps/pep-0257/ 2676 | [pep 440]: https://www.python.org/dev/peps/pep-0440/ 2677 | [pep 517]: https://www.python.org/dev/peps/pep-0517/ 2678 | [pep 518]: https://www.python.org/dev/peps/pep-0518/ 2679 | [pep 561]: https://www.python.org/dev/peps/pep-0561/ 2680 | [pep 8]: http://www.python.org/dev/peps/pep-0008/ 2681 | [pep8-naming codes]: https://github.com/pycqa/pep8-naming#pep-8-naming-conventions 2682 | [pep8-naming]: https://github.com/pycqa/pep8-naming 2683 | [pip install]: https://pip.pypa.io/en/stable/reference/pip_install/ 2684 | [pip]: https://pip.pypa.io/ 2685 | [pipx]: https://pipxproject.github.io/pipx/ 2686 | [poetry add]: https://python-poetry.org/docs/cli/#add 2687 | [poetry env]: https://python-poetry.org/docs/managing-environments/ 2688 | [poetry export]: https://python-poetry.org/docs/cli/#export 2689 | [poetry install]: https://python-poetry.org/docs/cli/#install 2690 | [poetry remove]: https://python-poetry.org/docs/cli/#remove 2691 | [poetry run]: https://python-poetry.org/docs/cli/#run 2692 | [poetry show]: https://python-poetry.org/docs/cli/#show 2693 | [poetry update]: https://python-poetry.org/docs/cli/#update 2694 | [poetry version]: https://python-poetry.org/docs/cli/#version 2695 | [poetry]: https://python-poetry.org/ 2696 | [pre-commit autoupdate]: https://pre-commit.com/#pre-commit-autoupdate 2697 | [pre-commit configuration]: https://pre-commit.com/#adding-pre-commit-plugins-to-your-project 2698 | [pre-commit repository-local hooks]: https://pre-commit.com/#repository-local-hooks 2699 | [pre-commit system hooks]: https://pre-commit.com/#system 2700 | [pre-commit-hooks]: https://github.com/pre-commit/pre-commit-hooks 2701 | [pre-commit]: https://pre-commit.com/ 2702 | [prettier]: https://prettier.io/ 2703 | [pycodestyle codes]: https://pycodestyle.pycqa.org/en/latest/intro.html#error-codes 2704 | [pycodestyle]: https://pycodestyle.pycqa.org/en/latest/ 2705 | [pydocstyle codes]: http://www.pydocstyle.org/en/stable/error_codes.html 2706 | [pydocstyle]: http://www.pydocstyle.org/ 2707 | [pyenv wiki]: https://github.com/pyenv/pyenv/wiki/Common-build-problems 2708 | [pyenv]: https://github.com/pyenv/pyenv 2709 | [pyflakes codes]: https://flake8.pycqa.org/en/latest/user/error-codes.html 2710 | [pyflakes]: https://github.com/PyCQA/pyflakes 2711 | [pygments]: https://pygments.org/ 2712 | [pypa/gh-action-pypi-publish]: https://github.com/pypa/gh-action-pypi-publish 2713 | [pypi]: https://pypi.org/ 2714 | [pyproject.toml]: https://python-poetry.org/docs/pyproject/ 2715 | [pytest layout]: https://docs.pytest.org/en/latest/explanation/goodpractices.html#choosing-a-test-layout-import-rules 2716 | [pytest]: https://docs.pytest.org/en/latest/ 2717 | [python build]: https://python-poetry.org/docs/cli/#build 2718 | [python package]: https://docs.python.org/3/tutorial/modules.html#packages 2719 | [python publish]: https://python-poetry.org/docs/cli/#publish 2720 | [python website]: https://www.python.org/ 2721 | [pyupgrade]: https://github.com/asottile/pyupgrade 2722 | [read the docs]: https://readthedocs.org/ 2723 | [readthedocs webhooks]: https://docs.readthedocs.io/en/stable/webhooks.html 2724 | [relative imports]: https://docs.python.org/3/reference/import.html#package-relative-imports 2725 | [release drafter]: https://github.com/release-drafter/release-drafter 2726 | [release-drafter/release-drafter]: https://github.com/release-drafter/release-drafter 2727 | [requirements file]: https://pip.readthedocs.io/en/stable/user_guide/#requirements-files 2728 | [restructuredtext]: https://docutils.sourceforge.io/rst.html 2729 | [safety]: https://github.com/pyupio/safety 2730 | [salsify/action-detect-and-tag-new-version]: https://github.com/salsify/action-detect-and-tag-new-version 2731 | [schlawack semantic]: https://hynek.me/articles/semver-will-not-save-you/ 2732 | [schreiner constraints]: https://iscinumpy.dev/post/bound-version-constraints/ 2733 | [schreiner poetry]: https://iscinumpy.dev/post/poetry-versions/ 2734 | [semantic versioning]: https://semver.org/ 2735 | [sphinx configuration]: https://www.sphinx-doc.org/en/master/usage/configuration.html 2736 | [sphinx-autobuild]: https://github.com/executablebooks/sphinx-autobuild 2737 | [sphinx-click]: https://sphinx-click.readthedocs.io/ 2738 | [sphinx]: http://www.sphinx-doc.org/ 2739 | [test fixture]: https://docs.pytest.org/en/latest/explanation/fixtures.html#about-fixtures 2740 | [testpypi]: https://test.pypi.org/ 2741 | [toml]: https://github.com/toml-lang/toml 2742 | [tox]: https://tox.readthedocs.io/ 2743 | [trailing-whitespace]: https://github.com/pre-commit/pre-commit-hooks#trailing-whitespace 2744 | [type annotations]: https://docs.python.org/3/library/typing.html 2745 | [typeguard]: https://github.com/agronholm/typeguard 2746 | [unix-style line endings]: https://en.wikipedia.org/wiki/Newline 2747 | [versions and constraints]: https://python-poetry.org/docs/dependency-specification/ 2748 | [virtual environment]: https://docs.python.org/3/tutorial/venv.html 2749 | [virtualenv]: https://virtualenv.pypa.io/ 2750 | [wheel]: https://www.python.org/dev/peps/pep-0427/ 2751 | [xdoctest]: https://github.com/Erotemic/xdoctest 2752 | [yaml]: https://yaml.org/ 2753 | --------------------------------------------------------------------------------