diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..65e442e --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,18 @@ +# To get started with Dependabot version updates, you'll need to specify which +# package ecosystems to update and where the package manifests are located. +# Please see the documentation for all configuration options: +# https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates + +version: 2 +updates: + - package-ecosystem: "pip" # See documentation for possible values + directory: "/" # Location of package manifests + schedule: + interval: "weekly" + reviewers: + - "@Hochfrequenz/python-developers-review-team" + # Maintain dependencies for GitHub Actions + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml new file mode 100644 index 0000000..27e6afb --- /dev/null +++ b/.github/workflows/codeql-analysis.yml @@ -0,0 +1,70 @@ +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +# +# ******** NOTE ******** +# We have attempted to detect the languages in your repository. Please check +# the `language` matrix defined below to confirm you have the correct set of +# supported CodeQL languages. +# +name: "CodeQL" + +on: + push: + branches: [main] + pull_request: + # The branches below must be a subset of the branches above + branches: [main] + schedule: + - cron: "29 14 * * 6" + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + matrix: + language: ["python"] + # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] + # Learn more about CodeQL language support at https://git.io/codeql-language-support + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v3 + with: + languages: ${{ matrix.language }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + # queries: ./path/to/local/query, your-org/your-repo/queries@main + + # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). + # If this step fails, then you should remove it and run the build manually (see below) + - name: Autobuild + uses: github/codeql-action/autobuild@v3 + + # ℹ️ Command-line programs to run using the OS shell. + # 📚 https://git.io/JvXDl + + # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines + # and modify them (or add more) to build your code if your project + # uses a compiled language + + #- run: | + # make bootstrap + # make release + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v3 diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml new file mode 100644 index 0000000..b7e0c94 --- /dev/null +++ b/.github/workflows/coverage.yml @@ -0,0 +1,26 @@ +name: "Coverage" + +on: + push: + branches: [main] + pull_request: {} +jobs: + coverage: + runs-on: ${{ matrix.os }} + strategy: + matrix: + python-version: ["3.12"] + os: [ubuntu-latest] + steps: + - uses: actions/checkout@v4 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install tox + - name: Run Tests and Record Coverage + run: | + tox -e coverage diff --git a/.github/workflows/dependabot_automerge.yml b/.github/workflows/dependabot_automerge.yml new file mode 100644 index 0000000..68d80b7 --- /dev/null +++ b/.github/workflows/dependabot_automerge.yml @@ -0,0 +1,18 @@ +name: Dependabot auto-approve / -merge +on: pull_request + +jobs: + dependabot: + permissions: + contents: write + pull-requests: write + runs-on: ubuntu-latest + env: + PR_URL: ${{github.event.pull_request.html_url}} + GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} + if: ${{ github.actor == 'dependabot[bot]' }} + steps: + - name: Approve a PR + run: gh pr review --approve "$PR_URL" + - name: Enable auto-merge for Dependabot PRs + run: gh pr merge --auto --squash "$PR_URL" diff --git a/.github/workflows/dev_test.yml b/.github/workflows/dev_test.yml new file mode 100644 index 0000000..462d93e --- /dev/null +++ b/.github/workflows/dev_test.yml @@ -0,0 +1,28 @@ +name: "Test Dev Environment" +# Checks that the dev environment (tox -e dev) can be set up. +# This might not work, if different linting/testing envs refer to different versions of the same lib (e.g. typing-extensions). +# Different versions of the same package might work for isolated specific envs (only linting, only testing...) but the dev environment inherits from all of them. +on: + push: + branches: [main] + pull_request: {} +jobs: + check: + runs-on: ${{ matrix.os }} + strategy: + matrix: + python-version: ["3.11", "3.12"] + os: [ubuntu-latest] + steps: + - uses: actions/checkout@v4 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + - name: Install Dependencies + run: | + python -m pip install --upgrade pip + pip install tox + - name: Create a Dev Environment + run: | + tox -e dev diff --git a/.github/workflows/formatting.yml b/.github/workflows/formatting.yml new file mode 100644 index 0000000..f9074e6 --- /dev/null +++ b/.github/workflows/formatting.yml @@ -0,0 +1,27 @@ +name: "Formatting" + +on: + push: + branches: [main] + pull_request: {} +jobs: + black: + runs-on: ${{ matrix.os }} + strategy: + matrix: + python-version: ["3.12"] + os: [ubuntu-latest] + tool: ["black", "isort"] + steps: + - uses: actions/checkout@v4 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r dev_requirements/requirements-formatting.txt + - name: ${{ matrix.tool }} Code Formatter + run: | + ${{ matrix.tool }} . --check diff --git a/.github/workflows/no_byte_order_mark.yml b/.github/workflows/no_byte_order_mark.yml new file mode 100644 index 0000000..30ad52b --- /dev/null +++ b/.github/workflows/no_byte_order_mark.yml @@ -0,0 +1,15 @@ +name: Prevent ByteOrderMarks + +on: + push: + branches: + - main + pull_request: {} + +jobs: + bom-check: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: arma-actions/bom-check@v1 + name: Check for BOM diff --git a/.github/workflows/packaging_test.yml b/.github/workflows/packaging_test.yml new file mode 100644 index 0000000..e41f1ff --- /dev/null +++ b/.github/workflows/packaging_test.yml @@ -0,0 +1,26 @@ +name: "Packaging Test" + +on: + push: + branches: [main] + pull_request: {} +jobs: + check_packaging: + runs-on: ${{ matrix.os }} + strategy: + matrix: + python-version: ["3.12"] + os: [ubuntu-latest] + steps: + - uses: actions/checkout@v4 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install tox + - name: Run Packaging Test + run: | + tox -e test_packaging diff --git a/.github/workflows/python-publish.yml b/.github/workflows/python-publish.yml new file mode 100644 index 0000000..6a3301d --- /dev/null +++ b/.github/workflows/python-publish.yml @@ -0,0 +1,62 @@ +# This GitHub workflow is only needed for python package releases which are supposed to be published on pypi. +# It requires the Github "environments" feature (see instructions below) it might not be available for private free accounts (but works for public or organization repos). +# After creating the "release" environment in the Github repo settings, you need to enter your Github organization/user name + repo name + "python-publish.yml" workflow file name in the PyPI UI to make this work. + +# This workflow uploads a Python Package using Twine when a release is created. +# For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries + +# name: Upload Python Package + +# on: +# release: +# types: [created, edited] + +# jobs: +# tests: +# if: startsWith(github.ref, 'refs/tags/v') +# runs-on: ${{ matrix.os }} +# strategy: +# matrix: +# python-version: ["3.12"] +# os: [ubuntu-latest] +# steps: +# - uses: actions/checkout@v4 +# - name: Set up Python ${{ matrix.python-version }} +# uses: actions/setup-python@v5 +# with: +# python-version: ${{ matrix.python-version }} +# - name: Install tox +# run: | +# python -m pip install --upgrade pip +# pip install tox +# - name: Run tox +# run: | +# tox + +# build-n-publish: +# name: Build and publish Python 🐍 distributions 📦 to PyPI and TestPyPI +# runs-on: ubuntu-latest +# # Specifying a GitHub environment, # Specifying a GitHub environment, which is strongly recommended by PyPI: https://docs.pypi.org/trusted-publishers/adding-a-publisher/ +# # you have to create an environment in your repository settings and add the environment name here +# environment: release +# permissions: +# # IMPORTANT: this permission is mandatory for trusted publishing +# id-token: write +# needs: tests +# steps: +# - uses: actions/checkout@v4 +# - name: Set up Python +# uses: actions/setup-python@v5 +# with: +# python-version: ${{ matrix.python-version }} +# - name: Install dependencies +# run: | +# python -m pip install --upgrade pip +# pip install -r dev_requirements/requirements-packaging.txt +# - name: Build wheel and source distributions +# run: | +# python -m build +# - name: Publish distribution 📦 to PyPI +# if: startsWith(github.ref, 'refs/tags/v') +# uses: pypa/gh-action-pypi-publish@release/v1 + diff --git a/.github/workflows/pythonlint.yml b/.github/workflows/pythonlint.yml new file mode 100644 index 0000000..d69821b --- /dev/null +++ b/.github/workflows/pythonlint.yml @@ -0,0 +1,28 @@ +name: "Linting" + +on: + push: + branches: [main] + pull_request: {} +jobs: + pylint: + name: Python Code Quality and Lint + runs-on: ${{ matrix.os }} + strategy: + matrix: + python-version: ["3.12"] + os: [ubuntu-latest] + linter-env: ["linting", "type_check", "spell_check"] + steps: + - uses: actions/checkout@v4 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + - name: Install Dependencies + run: | + python -m pip install --upgrade pip + pip install tox + - name: Run ${{ matrix.linter-env }} via Tox + run: | + tox -e ${{ matrix.linter-env }} diff --git a/.github/workflows/unittests.yml b/.github/workflows/unittests.yml new file mode 100644 index 0000000..3edd536 --- /dev/null +++ b/.github/workflows/unittests.yml @@ -0,0 +1,26 @@ +name: "Unittests" + +on: + push: + branches: [main] + pull_request: {} +jobs: + pytest: + runs-on: ${{ matrix.os }} + strategy: + matrix: + python-version: ["3.11", "3.12"] + os: [ubuntu-latest] + steps: + - uses: actions/checkout@v4 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + - name: Install Dependencies + run: | + python -m pip install --upgrade pip + pip install tox + - name: Run the Unit Tests via Tox + run: | + tox -e tests diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..fb33be7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,136 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +pip-wheel-metadata/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +.python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +.idea/ + +# vscode settings +.vscode/ + +src/_your_package_version.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..c020b3b --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,24 @@ +# to update all repo revisions just run: pre-commit autoupdate +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.4.0 + hooks: + - id: check-yaml + - id: end-of-file-fixer + - id: trailing-whitespace + - repo: https://github.com/psf/black + rev: 23.9.1 + hooks: + - id: black + language_version: python3 + - repo: https://github.com/pycqa/isort + rev: 5.12.0 + hooks: + - id: isort + name: isort (python) + - id: isort + name: isort (cython) + types: [cython] + - id: isort + name: isort (pyi) + types: [pyi] diff --git a/README.md b/README.md new file mode 100644 index 0000000..d1a90ed --- /dev/null +++ b/README.md @@ -0,0 +1,153 @@ +# Python Template Repository including a `tox.ini`, Unittests&Coverage, Pylint & MyPy Linting Actions and a PyPI Publishing Workflow + + + +![Unittests status badge](https://github.com/Hochfrequenz/python_template_repository/workflows/Unittests/badge.svg) +![Coverage status badge](https://github.com/Hochfrequenz/python_template_repository/workflows/Coverage/badge.svg) +![Linting status badge](https://github.com/Hochfrequenz/python_template_repository/workflows/Linting/badge.svg) +![Black status badge](https://github.com/Hochfrequenz/python_template_repository/workflows/Formatting/badge.svg) + +This is a template repository. +It doesn't contain any useful code but only a minimal working setup for a Python project including: + +- a basic **project structure** with + - tox.ini + - `pyproject.toml` where the project metadata and dependencies are defined + - and a requirements.txt derived from it + - an example class + - an example unit test (using pytest) +- ready to use **Github Actions** for + - [pytest](https://pytest.org) + - [code coverage measurement](https://coverage.readthedocs.io) (fails below 80% by default) + - [pylint](https://pylint.org/) (only accepts 10/10 code rating by default) + - [mypy](https://github.com/python/mypy) (static type checks where possible) + - [black](https://github.com/psf/black) code formatter check + - [isort](https://pycqa.github.io/isort/) import order check + - [codespell](https://github.com/codespell-project/codespell) spell check (including an ignore list) + - autoresolve dev-dependencies with `tox -e compile_requirements` + - ready-to-use publishing workflow for pypi (see readme section below) + +By default, it uses Python version 3.12. + +This repository uses a [`src`-based layout](https://packaging.python.org/en/latest/discussions/src-layout-vs-flat-layout/). +This approach has many advantages and basically means for developers, that all business logic lives in the `src` directory. + +## How to use this Repository on Your Machine + +This introduction assumes that you have tox installed already ( +see [installation instructions](https://tox.readthedocs.io/en/latest/installation.html)) and that a `.toxbase` environment +has been created. +`.toxbase` is a project independent virtual environment-template for all the tox environments on your machine. If anything is weird during the tox installation or after the installation, try turning your computer off and on again before getting too frustrated. + +### Powershell restrictions on Windows +Also on new windows machines it is possible that the execution policy is set to restricted and you are not allowed execute scripts. You can find detailed information [here](https://learn.microsoft.com/de-de/powershell/module/microsoft.powershell.core/about/about_execution_policies?view=powershell-7.3). + +The quickest way to solve this problem: Open an Administrator Powershell (e.g. Windows PowerShell App) +```ps +Set-ExecutionPolicy -ExecutionPolicy AllSigned +``` +and try again (with your regular user, not as admin). + +### Creating the project-specifc dev environment. +If all problems are solved and you're ready to start: + 1. clone the repository, you want to work in + 2. create the `dev` environment on your machine. To do this: + a) Open a Powershell + b) change directory to your repository +and finally type + +```bash +tox -e dev +``` + +You have now created the development environment (dev environment). It is the environment which contains both the usual requirements as well as the testing and linting tools. + +### How to use with PyCharm + +1. You have cloned the repository, you want to work in, and have created the virtual environment, in which the repository should be executed (`your_repo/.tox/dev`). Now, to actually work inside the newly created environment, you need to tell PyCharm (your IDE) that it should use the virtual environment - to be more precise: the interpreter of this dev environment. How to do this: +a) navigate to: File ➡ Settings (Strg + Alt + S) ➡ Project: your_project ➡ Python Interpreter ➡ Add interpreter ➡ Existing +b) Choose as interpreter: `your_repo\.tox\dev\Scripts\python.exe` (under windows) +2. Set the default test runner of your project to pytest. How to do it: +a) navigate to Files ➡ Settings ➡ Tools ➡ Python integrated tools ➡ Testing: Default test runner +b) Change to "pytest" +If this doesn't work anymore, see [the PyCharm docs](https://www.jetbrains.com/help/pycharm/choosing-your-testing-framework.html) +3. Set the `src` directory as sources root. How to do this: +right click on 'src' ➡ "Mark directory as…" ➡ sources root +If this doesn't work anymore, see: [PyCharm docs](https://www.jetbrains.com/help/pycharm/content-root.html). +Setting the `src` directory right, allows PyCharm to effectively suggest import paths. +If you ever see something like `from src.mypackage.mymodule import ...`, then you probably forgot this step. +5. Set the working directory of the unit tests to the project root (instead of the unittest directory). How to do this: +a) Open any test file whose name starts with `test_` in unit tests/tests +b) Right click inside the code ➡ More Run/Debug ➡ Modify Run Configuration ➡ expand Environment collapsible ➡ Working directory +c) Change to `your_repo` instead of `your_repo\unittests` +By doing so, the import and other file paths in the tests are relative to the repo root. +If this doesn't work anymore, see: [working directory of the unit tests](https://www.jetbrains.com/help/pycharm/creating-run-debug-configuration-for-tests.html) + +### How to use with VS Code +All paths mentioned in this section are relative to the repository root. + +1. Open the folder with VS Code. +2. **Select the python interpreter** ([official docs](https://code.visualstudio.com/docs/python/environments#_manually-specify-an-interpreter)) which is created by tox. Open the command pallett with `CTRL + P` and type `Python: Select Interpreter`. Select the interpreter which is placed in `.tox/dev/Scripts/python.exe` under Windows or `.tox/dev/bin/python` under Linux and macOS. +3. **Set up pytest and pylint**. Therefore we open the file `.vscode/settings.json` which should be automatically generated during the interpreter setup. If it doesn't exist, create it. Insert the following lines into the settings: + +```json +{ + "python.testing.unittestEnabled": false, + "python.testing.nosetestsEnabled": false, + "python.testing.pytestEnabled": true, + "pythonTestExplorer.testFramework": "pytest", + "python.testing.pytestArgs": ["unittests"], + "python.linting.pylintEnabled": true +} +``` + +4. Create a `.env` file and insert the following line + +For Windows: + +``` +PYTHONPATH=src;${PYTHONPATH} +``` + +For Linux and Mac: + +``` +PYTHONPATH=src:${PYTHONPATH} +``` + +This makes sure, that the imports are working for the unittests. +At the moment I am not totally sure that it is the best practise, but it's getting the job done. + +5. Enjoy 🤗 + +## Publishing on PyPI + +This repository contains all necessary CI steps to publish any project created from it on PyPI. +It uses the trusted publishers workflow as described in the [official Python documentation](https://packaging.python.org/guides/publishing-package-distribution-releases-using-github-actions-ci-cd-workflows/). +It just requires some manual adjustments/settings depending on your project: + +1. Fill out the metadata in the [`pyproject.toml`](pyproject.toml); Namely the package name and the dependencies which should be in sync with your `requirements.in`. +2. Uncomment the lines in [`.github/workflows/python-publish.yml`](.github/workflows/python-publish.yml) +3. Create a [new environment in your GitHub repository](https://github.com/Hochfrequenz/python_template_repository/settings/environments) and call it `release`. +4. Set up a new trusted publisher [in your PYPI account](https://pypi.org/manage/account/publishing/). + 1. PyPI Project Name: The name which you defined in the `pyproject.toml` is the name of the project which you have to enter here. + 2. Owner: The GitHub organization name or GitHub username that owns the repository + 3. Repository name: The name of the GitHub repository that contains the publishing workflow + 4. Workflow name: The filename of the publishing workflow. This file should exist in the .github/workflows/ directory in the repository configured above. Here in our case: `python-publish.yml` + 5. Environment name: The name of the GitHub Actions environment that the above workflow uses for publishing. Here in our case: `release` +5. Now create a release by clicking on "Create new release" in the right Github sidebar (or visit `github.com/your-username/your-reponame/releases/new`). This should trigger the workflow (see the "Actions" tab of your repo). +6. Check if the action failed. If it succeeded your PyPI account should now show the new project. It might take some minutes until the package can be installed via `pip install packagename` because the index has to be updated. +7. Now create another PyPI token with limited scope and update the Github repository secret accordingly. + +## Contribute + +You are very welcome to contribute to this template repository by opening a pull request against the main branch. + +### GitHub Actions + +- Dependabot auto-approve / -merge: + - If the actor is the Dependabot bot (i.e. on every commit by Dependabot) + the pull request is automatically approved and auto merge gets activated + (using squash merge). + Note that if you haven't enabled "auto merge" for your repository, the auto merge activation will fail. + If you want to use a merge type other than "squash merge" you have to edit the workflow. diff --git a/dev_requirements/requirements-coverage.in b/dev_requirements/requirements-coverage.in new file mode 100644 index 0000000..d1e1a80 --- /dev/null +++ b/dev_requirements/requirements-coverage.in @@ -0,0 +1,2 @@ +# specific requirements for the tox coverage env +coverage diff --git a/dev_requirements/requirements-coverage.txt b/dev_requirements/requirements-coverage.txt new file mode 100644 index 0000000..d65dd70 --- /dev/null +++ b/dev_requirements/requirements-coverage.txt @@ -0,0 +1,9 @@ +# SHA1:6dafbcf610e9f81897b65ee9142715ab2e793f9e +# +# This file is autogenerated by pip-compile-multi +# To update, run: +# +# pip-compile-multi +# +coverage==7.5.1 + # via -r dev_requirements/requirements-coverage.in diff --git a/dev_requirements/requirements-formatting.in b/dev_requirements/requirements-formatting.in new file mode 100644 index 0000000..41e6b86 --- /dev/null +++ b/dev_requirements/requirements-formatting.in @@ -0,0 +1,3 @@ +# specific requirements for the formatting envs +black +isort diff --git a/dev_requirements/requirements-formatting.txt b/dev_requirements/requirements-formatting.txt new file mode 100644 index 0000000..2e04017 --- /dev/null +++ b/dev_requirements/requirements-formatting.txt @@ -0,0 +1,21 @@ +# SHA1:2c7ffcd29222de3114c7f7994911f1b69d06b6b3 +# +# This file is autogenerated by pip-compile-multi +# To update, run: +# +# pip-compile-multi +# +black==24.4.2 + # via -r dev_requirements/requirements-formatting.in +click==8.1.7 + # via black +isort==5.13.2 + # via -r dev_requirements/requirements-formatting.in +mypy-extensions==1.0.0 + # via black +packaging==24.0 + # via black +pathspec==0.12.1 + # via black +platformdirs==4.2.0 + # via black diff --git a/dev_requirements/requirements-linting.in b/dev_requirements/requirements-linting.in new file mode 100644 index 0000000..787ac85 --- /dev/null +++ b/dev_requirements/requirements-linting.in @@ -0,0 +1,2 @@ +# specific requirements for the tox linting env +pylint diff --git a/dev_requirements/requirements-linting.txt b/dev_requirements/requirements-linting.txt new file mode 100644 index 0000000..15b8260 --- /dev/null +++ b/dev_requirements/requirements-linting.txt @@ -0,0 +1,21 @@ +# SHA1:0e15f8789b9d62fe90d1f1b0b6a7e32f13b99b19 +# +# This file is autogenerated by pip-compile-multi +# To update, run: +# +# pip-compile-multi +# +astroid==3.2.2 + # via pylint +dill==0.3.8 + # via pylint +isort==5.13.2 + # via pylint +mccabe==0.7.0 + # via pylint +platformdirs==4.2.0 + # via pylint +pylint==3.2.2 + # via -r dev_requirements/requirements-linting.in +tomlkit==0.12.4 + # via pylint diff --git a/dev_requirements/requirements-packaging.in b/dev_requirements/requirements-packaging.in new file mode 100644 index 0000000..3aad7fa --- /dev/null +++ b/dev_requirements/requirements-packaging.in @@ -0,0 +1,3 @@ +# requirements for the hatchling build ssystem +build +twine diff --git a/dev_requirements/requirements-packaging.txt b/dev_requirements/requirements-packaging.txt new file mode 100644 index 0000000..6a4969c --- /dev/null +++ b/dev_requirements/requirements-packaging.txt @@ -0,0 +1,72 @@ +# +# This file is autogenerated by pip-compile with Python 3.12 +# by the following command: +# +# pip-compile dev_requirements/requirements-packaging.in +# +build==1.2.1 + # via -r requirements-packaging.in +certifi==2024.2.2 + # via requests +cffi==1.16.0 + # via cryptography +charset-normalizer==3.3.2 + # via requests +cryptography==42.0.5 + # via secretstorage +docutils==0.20.1 + # via readme-renderer +idna==3.7 + # via requests +importlib-metadata==7.0.2 + # via twine +jaraco-classes==3.3.1 + # via keyring +jeepney==0.8.0 + # via + # keyring + # secretstorage +keyring==24.3.1 + # via twine +markdown-it-py==3.0.0 + # via rich +mdurl==0.1.2 + # via markdown-it-py +more-itertools==10.2.0 + # via jaraco-classes +nh3==0.2.15 + # via readme-renderer +packaging==24.0 + # via build +pkginfo==1.10.0 + # via twine +pycparser==2.21 + # via cffi +pygments==2.17.2 + # via + # readme-renderer + # rich +pyproject-hooks==1.0.0 + # via build +readme-renderer==43.0 + # via twine +requests==2.32.0 + # via + # requests-toolbelt + # twine +requests-toolbelt==1.0.0 + # via twine +rfc3986==2.0.0 + # via twine +rich==13.7.1 + # via twine +secretstorage==3.3.3 + # via keyring +twine==5.1.0 + # via -r requirements-packaging.in +urllib3==2.2.1 + # via + # requests + # twine +zipp==3.18.1 + # via importlib-metadata diff --git a/dev_requirements/requirements-spell_check.in b/dev_requirements/requirements-spell_check.in new file mode 100644 index 0000000..e3da9e4 --- /dev/null +++ b/dev_requirements/requirements-spell_check.in @@ -0,0 +1 @@ +codespell diff --git a/dev_requirements/requirements-spell_check.txt b/dev_requirements/requirements-spell_check.txt new file mode 100644 index 0000000..91e7ed2 --- /dev/null +++ b/dev_requirements/requirements-spell_check.txt @@ -0,0 +1,9 @@ +# SHA1:a7062511e9640ed2a5c2dea1da5c8b5f59c0f0d7 +# +# This file is autogenerated by pip-compile-multi +# To update, run: +# +# pip-compile-multi +# +codespell==2.2.6 + # via -r dev_requirements\requirements-spell_check.in diff --git a/dev_requirements/requirements-tests.in b/dev_requirements/requirements-tests.in new file mode 100644 index 0000000..5497961 --- /dev/null +++ b/dev_requirements/requirements-tests.in @@ -0,0 +1,2 @@ +# specific requirements for the tox tests env +pytest diff --git a/dev_requirements/requirements-tests.txt b/dev_requirements/requirements-tests.txt new file mode 100644 index 0000000..581108f --- /dev/null +++ b/dev_requirements/requirements-tests.txt @@ -0,0 +1,15 @@ +# SHA1:0eaa389e1fdb3a1917c0f987514bd561be5718ee +# +# This file is autogenerated by pip-compile-multi +# To update, run: +# +# pip-compile-multi +# +iniconfig==2.0.0 + # via pytest +packaging==24.0 + # via pytest +pluggy==1.5.0 + # via pytest +pytest==8.2.1 + # via -r dev_requirements/requirements-tests.in diff --git a/dev_requirements/requirements-type_check.in b/dev_requirements/requirements-type_check.in new file mode 100644 index 0000000..4446a91 --- /dev/null +++ b/dev_requirements/requirements-type_check.in @@ -0,0 +1,2 @@ +# specific requirements for the tox type_check environment +mypy diff --git a/dev_requirements/requirements-type_check.txt b/dev_requirements/requirements-type_check.txt new file mode 100644 index 0000000..3f275a5 --- /dev/null +++ b/dev_requirements/requirements-type_check.txt @@ -0,0 +1,13 @@ +# SHA1:7983aaa01d64547827c20395d77e248c41b2572f +# +# This file is autogenerated by pip-compile-multi +# To update, run: +# +# pip-compile-multi +# +mypy==1.10.0 + # via -r dev_requirements/requirements-type_check.in +mypy-extensions==1.0.0 + # via mypy +typing-extensions==4.10.0 + # via mypy diff --git a/domain-specific-terms.txt b/domain-specific-terms.txt new file mode 100644 index 0000000..ee2a9a2 --- /dev/null +++ b/domain-specific-terms.txt @@ -0,0 +1 @@ +# contains 1 lower case word per line which are ignored in the spell_check diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..67de3ed --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,65 @@ +[project] +name = "your-favourite-package-name" +description = "Description of your package" +license = { text = "MIT" } +requires-python = ">=3.11" +authors = [{ name = "your name", email = "your@email.address" }] +keywords = ["your", "important", "keywords"] +classifiers = [ + "Development Status :: 4 - Beta", + "Environment :: Console", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", + "Programming Language :: Python", + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", +] +dependencies = [] # add all the dependencies here +dynamic = ["readme", "version"] + +[project.urls] +Changelog = "https://github.com/Hochfrequenz/python_template_repository/releases" +Homepage = "https://github.com/Hochfrequenz/python_template_repository" + +[tool.black] +line-length = 120 +target_version = ["py311", "py312"] + +[tool.isort] +line_length = 120 +profile = "black" + +[tool.pylint."MESSAGES CONTROL"] +max-line-length = 120 + +[mypy] +truethy-bool = true + +[tool.mypy] +disable_error_code = ["no-untyped-def", "no-untyped-call"] + +[build-system] +requires = ["hatchling>=1.8.0", "hatch-vcs", "hatch-fancy-pypi-readme"] +build-backend = "hatchling.build" + +[tool.hatch.metadata.hooks.fancy-pypi-readme] +content-type = "text/markdown" +fragments = [{ path = "README.md" }] + +[tool.hatch.version] +source = "vcs" + +[tool.hatch.build.hooks.vcs] +version-file = "src/_your_package_version.py" +template = ''' +version = "{version}" +''' + +[tool.hatch.build.targets.sdist] +exclude = ["/unittests"] + +[tool.hatch.build.targets.wheel] +only-include = ["src"] +sources = ["src"] diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..79ca470 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,6 @@ +# +# This file is autogenerated by pip-compile with Python 3.11 +# by the following command: +# +# pip-compile pyproject.toml +# diff --git a/src/mypackage/__init__.py b/src/mypackage/__init__.py new file mode 100644 index 0000000..c8dd36b --- /dev/null +++ b/src/mypackage/__init__.py @@ -0,0 +1,3 @@ +""" +src contains all your business logic +""" diff --git a/src/mypackage/mymodule.py b/src/mypackage/mymodule.py new file mode 100644 index 0000000..2afc797 --- /dev/null +++ b/src/mypackage/mymodule.py @@ -0,0 +1,23 @@ +""" +This a docstring for the module. +""" + + +class MyClass: # pylint: disable=too-few-public-methods + """ + This is a docstring for the class. + """ + + def __init__(self): + """ + Initialize for the sake of initializing + """ + self.my_instance_var: str = "abc" + + def do_something(self) -> str: + """ + Actually does nothing. + :return: the value of an instance variable + """ + # this is a super long line with: 100 < line length <= 120 to demonstrate the purpose of pyproject.toml + return self.my_instance_var diff --git a/src/mypackage/py.typed b/src/mypackage/py.typed new file mode 100644 index 0000000..65a800b --- /dev/null +++ b/src/mypackage/py.typed @@ -0,0 +1,2 @@ +# This file marks mypackage as PEP561 compatible. +# Further reading: https://mypy.readthedocs.io/en/stable/installed_packages.html#creating-pep-561-compatible-packages diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..00db660 --- /dev/null +++ b/tox.ini @@ -0,0 +1,95 @@ +[tox] +envlist = + tests + linting + coverage + type_check +skip_missing_interpreters = True +skipsdist = True + +[testenv] +commands = python -m pip install --upgrade pip + +[testenv:tests] +# the tests environment is called by the Github action that runs the unit tests +deps = + -r requirements.txt + -r dev_requirements/requirements-tests.txt +setenv = PYTHONPATH = {toxinidir}/src +commands = python -m pytest --basetemp={envtmpdir} {posargs} + +[testenv:linting] +# the linting environment is called by the Github Action that runs the linter +deps = + {[testenv:tests]deps} + -r dev_requirements/requirements-linting.txt + # add your fixtures like e.g. pytest_datafiles here +setenv = PYTHONPATH = {toxinidir}/src +commands = + pylint mypackage + pylint unittests --rcfile=unittests/.pylintrc + # add single files (ending with .py) or packages here + +[testenv:type_check] +# the type_check environment checks the type hints using mypy +setenv = PYTHONPATH = {toxinidir}/src +deps = + {[testenv:tests]deps} + -r dev_requirements/requirements-type_check.txt +commands = + mypy --show-error-codes src/mypackage --strict + mypy --show-error-codes unittests --strict + # add single files (ending with .py) or packages here + +[testenv:spell_check] +# the spellcheck environment checks the code for typos +setenv = PYTHONPATH = {toxinidir}/src +deps = + -r requirements.txt + -r dev_requirements/requirements-spell_check.txt +commands = + codespell --ignore-words=domain-specific-terms.txt src + codespell --ignore-words=domain-specific-terms.txt README.md + # add single files (ending with .py) or packages here + +[testenv:coverage] +# the coverage environment is called by the Github Action that runs the coverage measurement +changedir = unittests +deps = + {[testenv:tests]deps} + -r dev_requirements/requirements-coverage.txt +setenv = PYTHONPATH = {toxinidir}/src +commands = + coverage run -m pytest --basetemp={envtmpdir} {posargs} + coverage html --omit .tox/*,unittests/* + coverage report --fail-under 80 --omit .tox/*,unittests/* + +[testenv:compile_requirements] +deps = + pip-compile-multi +commands = + pip-compile-multi -d dev_requirements --autoresolve + +[testenv:dev] +# the dev environment contains everything you need to start developing on your local machine. +deps = + {[testenv:tests]deps} + {[testenv:linting]deps} + {[testenv:type_check]deps} + {[testenv:coverage]deps} + {[testenv:spell_check]deps} + -r dev_requirements/requirements-formatting.txt + pip-tools + pre-commit +commands = + python -m pip install --upgrade pip + pip install -r requirements.txt + pre-commit install + +[testenv:test_packaging] +skip_install = true +deps = + -r dev_requirements/requirements-packaging.txt +commands = + python -m build + twine check dist/* diff --git a/unittests/.pylintrc b/unittests/.pylintrc new file mode 100644 index 0000000..cc123a6 --- /dev/null +++ b/unittests/.pylintrc @@ -0,0 +1,11 @@ +[pylint] +# for the unittests disable some pylint warnings, e.g. we don't want to force devs to write docstrings for every test +disable = + C0114, # disable missing-module-docstring + C0115, # disable missing-class-docstring + C0116, # disable missing-function-docstring + R0903, # disable too-few-public-methods + W0621, # disable redefined-outer-name (this is for the use of pytest fixtures) + R0801, # disable duplicate-code (we prefer our test to be explicit rather than perfectly redundancy free) +[pylint."MESSAGES CONTROL"] +max-line-length = 120 \ No newline at end of file diff --git a/unittests/__init__.py b/unittests/__init__.py new file mode 100644 index 0000000..10eb880 --- /dev/null +++ b/unittests/__init__.py @@ -0,0 +1,4 @@ +""" +This file is here, because this allows for best de-coupling of tests and application/library logic. +Further reading: https://docs.pytest.org/en/6.2.x/goodpractices.html#tests-outside-application-code +""" diff --git a/unittests/test_myclass.py b/unittests/test_myclass.py new file mode 100644 index 0000000..768a9e4 --- /dev/null +++ b/unittests/test_myclass.py @@ -0,0 +1,11 @@ +from mypackage.mymodule import MyClass + + +class TestMyClass: + """ + A class with pytest unit tests. + """ + + def test_something(self): + my_class = MyClass() + assert my_class.do_something() == "abc"