Tools for prettier Python projects

Für die meisten dieser Tools gibt es auch Alternativen. Die hier genannten ließen sich am einfachsten in unsere bestehenden Worflows integrieren.

Style-Guides

Die älteren unter uns werden sich noch an endlose Diskussionen erinnern, in denen Tab-Einrückungen oder die Art der Anführungszeichen bei Strings mit religiösem Eifer ausgefochten wurden.

PEP-8 beendet schon seit fast zehn Jahren viele solcher Diskussionen, aber wirklich los wird man sie erst, wenn ein Linter wirklich keine zwei Möglichkeiten zulässt den gleichen Code zu schreiben.

Hier kommt Black ins Spiel:

Black is the uncompromising Python code formatter. By using it, you agree to cede control over minutiae of hand-formatting. In return, Black gives you speed, determinism, and freedom from pycodestyle nagging about formatting. You will save time and mental energy for more important matters.

Für ein Team von ansonsten diskussionsfreudigen Entwicklern ist das wirklich ein Segen.

Black hat allerdings einen blinden Fleck, wenn es um unnötige aber valide Statements geht: nicht verwendete Variablen, unnötige Imports et cetera. Dafür braucht man dann doch noch einen Linter wie flake8, dem man im Zweifel aber auch per

.filter(column == True)  # NOQA: SQLAlchemy

zu verstehen geben kann: Ja, das soll so.

Mitunter kommt es vor, dass flake8 und Black unterschiedlicher Auffassung sind. Black ist hier absolut kompromisslos:

While Black enforces formatting that conforms to PEP 8, other tools may raise warnings about Black's changes or will overwrite Black's changes. A good example of this is isort. Since Black is barely configurable, these tools should be configured to neither warn about nor overwrite Black's changes.

Bei flake8 empfiehlt sich folgende Ergänzung in der .flake8 oder setup.cfg:

[flake8]
max-line-length = 88
extend-ignore = E203

Für andere Tools gibt es in der Black-Dokumentation Konfigurationsvorschläge.

Darker: Black auf Änderungen beschränken

Wenn die Einführung von Black zu viel Chaos bedeuten würde (insbesondere bei größeren Projekten mit mehreren Maintainern), kann Darker sich auf neuen oder geänderten Code beschränken.

Grundsätzlich gilt aber natürlich auch hier PEP 8:

A style guide is about consistency. Consistency with this style guide is important. Consistency within a project is more important. Consistency within one module or function is the most important.

In anderen Worten: auch Darker sollte nur dort zum Einsatz kommen, wo es keine zusätzliche Inkonsistenz zum bestehenden Code erzeugt. Bisher kommt Darker bei und nicht zum Einsatz, da wir die Einführung von Black bisher gut koordinieren konnten.

Abhängigkeitsmanagement

pip-tools

pip-tools kann Abhängigkeiten aus einer setup.py (oder setup.cfg) extrahieren und in einer requirements.txt einfrieren.

Das ist sinnvoll um die Liste der direkten Abhängigkeiten in der setup.py klein zu halten und die verwendeten Versionen in der Git-History festzuhalten.

Unser setup ist hier im wesentlichen wie bei GitHub dokumentiert, insbesondere nutzen wir auch die dev-requirements.in um nicht nur Installationsabhängigkeiten, sondern auch Entwickler-Tools einfach installierbar zu machen.

Neben Framework-spezifischen Tools stellt folgendes die Basis einer dev-requirements.in dar:

-c requirements.txt
flake8
pytest
pre-commit
pip-tools
black

Wenn man bei uns ein bestehendes Python-Repository klont sind die ersten Schritte also in der Regel:

python3 -m venv venv
venv/bin/pip install -r requirements.txt -r dev-requirements.txt
venv/bin/pre-commit install

renovate-bot

Der renovate-bot wird zwar im Repo konfiguriert aber wie der Name suggeriert auf dem GitLab-Server ausgeführt. Indirekt sorgt er vor allem für eine gute Code-Coverage in den Unittests da er ständig Dinge kaputt macht und -- wenn man das nicht in den Unittests merkt -- auch automatisch mergen kann.

Wir haben auto-merges für Minor Versions aktiviert, man kann das auch ganz abstellen, dann bekommt man aber sehr viele Merge Requests.

Trotz der zusätzlichen Arbeit ist dieser oder ein ähnlicher Bot unerlässlich um zu verhindern, dass ein Repository schleichend nicht mehr wartbar oder unsicher wird.

Pre-Commit hooks

Pre-commit hooks sind kleine Programme, in der Regel Shell-Scripts, die von Git vor einem Commit ausgeführt werden. Man kann sie verwenden um Validierungen durchzuführen und den Commit gegebenenfalls abzubrechen.

Das pre-commit Python-Paket stellt ein solches Programm bereit mit dem sich eine Reihe von Validierungen konfigurieren lässt. Da die Konfigurationsdatei im Repository liegt können alle Entwickler die gleichen Standards befolgen (erzwungen wird es per GitLab-CI).

Da die Website wohl immer aktueller sein wird als ein Blog-Post will ich hier die hervorragende Dokumentation nicht wiederholen.

Unsere Beispielkonfiguration (.pre-commit-config.yaml):

fail_fast: true
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
  rev: master
  hooks:
    - id: no-commit-to-branch
    - id: check-merge-conflict
    - id: check-symlinks
    - id: mixed-line-ending
      args: ['--fix=no']
    - id: check-ast
    - id: check-builtin-literals
    - id: check-case-conflict
    - id: check-docstring-first
- repo: https://github.com/asottile/pyupgrade
  rev: master
  hooks:
    - id: pyupgrade
      args: [--py37-plus]
- repo: local
  hooks:
    - id: black
      name: black
      entry: black
      language: python
      files: '.*\.py$'
    - id: flake8
      name: flake8
      entry: flake8
      language: python
      pass_filenames: false

Lessons learned

In der Quick-Start-Anleitung wird Black direkt vom Repository eingebunden. Das ist nicht verkehrt, wenn man die rev auf eine bestimmte Version pinnt und nicht zusätzlich über die dev-requirements.txt installiert. Um die Version aktuell und konsistent zu halten, haben wir uns entschieden Black und Flake8 immer im lokalen venv zu installieren und so auch anderen Tools die selbe Version bereitzustellen.

In der .pre-commit-config.yaml wird mit repo: local der Befehl in entry lokal ausgeführt. Das funktioniert also nur mit aktivem venv.

Ausblick: Type Annotations

Die oben beschriebenen Tools und Anpassungen lassen sich mit überschaubarem Aufwand auch auf bestehende Projekte anwenden. Die Einführung von Type Annotations ist jedoch immer mit sorgfältiger Handarbeit verbunden.

Bei bestehenden Projekten verwenden wir die Semantik aus PEP 484 vor allem für neuen Code und als Dokumentation.

Bei neuen Projekten kann man dann noch einen Schritt weiter gehen und mit Tools wie Mypy oder pyright die Annotationen nutzen um ganze Fehlerklassen automatisch zu erkennen und zu unterbinden.

written by Milan Oberkirch | 11/17/2020
More on this topic
7 min reading time › | Blog

Using Redis Subscriptions efficiently in Python

Inspired by the websockets broadcast feature we built a subscription multiplexer for redis subscriptions to subscribe to Redis channels and patterns once for all relevant clients.

read more
8 min reading time › | Blog

Adding type hints to existing code in Python

The Python interpreter handles types in a dynamic and flexible way without constraints on what type of object a variable is assigned to. Since Python 3.5 programmers have the option to add type annotations to their code. Here we how it's done.

read more
3 min reading time › | Blog

Set up Django to only allow CORS requests in DEBUG mode

This post is about how to set up a Django project to only allow CORS requests in DEBUG mode, even if they require a login to the backend. In our case, this has been useful to test frontend customizations on the internal dev environment without having to start the backend locally.

read more
2 min reading time › | Blog

From backend to frontend: we are looking for reinforcement

We have two vacancies for Fullstack or Backend Developers for our offices in Freiburg and Olten. If you want to bring your passion to a strong team, then you should apply.

read more
3 min reading time › | Blog

Migrating from enzyme to testing-library/react

We have rewritten our frontend unit tests from using enzyme to testing-library/react. This article provides a quick overview of the updates.

read more
2 min reading time › | Blog

mapset 2.0 with MUI

mapset 2.0 has been widely refactored using the open-source react component library MUI (Material-UI).

read more

Contact

geOps AG
Solothurnerstrasse 235
CH-4600 Olten

fon: +41 61 588 05 05
mail: info@geops.ch
geOps GmbH
Bismarckallee 10
D-79098 Freiburg im Breisgau

fon: +49 761 458 925 0
mail: info@geops.de
Imprint | Privacy | Terms of service