Files
airflow/contributing-docs/08_static_code_checks.rst
T
Jarek Potiuk 4f3b228be1 Isolate non-provider mypy hooks per distribution with dedicated .build/ venvs (#65492)
* Isolate mypy prek hooks, cover all non-provider dirs, and clean up type errors

Each non-provider mypy prek hook now builds and caches its own virtualenv
at .build/mypy-venvs/<hook>/ and its own mypy cache at
.build/mypy-caches/<hook>/. UV_PROJECT_ENVIRONMENT redirects uv away from
the project's .venv, so running the hook never mutates a contributor's
regular development environment while still matching CI's frozen
dependency set. Mypy runs with --follow-imports=silent so each hook only
reports errors for files it owns; transitive code is covered by its own
hook and different venvs no longer produce divergent results on shared
code.

Adds mypy hooks for the non-provider directories that were previously
uncovered: airflow-ctl-tests, helm-tests, airflow-e2e-tests,
task-sdk-integration-tests, docker-tests, kubernetes-tests, and shared.
The mypy-shared hook iterates every shared/<dist> workspace distribution
and builds a separate venv + cache per distribution so each shared
library is type-checked against its own dependency set.

breeze down --cleanup-mypy-cache additionally removes
.build/mypy-venvs/ and .build/mypy-caches/ so all per-hook state is
wiped alongside the existing .mypy_cache and mypy-cache-volume.

Also fixes pre-existing type errors surfaced by the newly added and
cleaned-up checks: platform-specific ignores for Linux-only
os.posix_fadvise in the shared logging helper, narrower types and
type: ignore where appropriate in shared configuration/observability/
timezones/secrets_backend/secrets_masker, Liskov override markers on
the AirflowConfigParser subclass methods, and small correctness fixes
in dev/breeze and the docker-tests / kubernetes-tests helpers so the
full non-provider mypy suite runs clean on macOS and in CI.

* Move mypy prek hooks to their respective distribution configs

The new mypy hooks for airflow-ctl-tests, helm-tests, airflow-e2e-tests,
task-sdk-integration-tests, docker-tests, and kubernetes-tests now live
in each distribution's own .pre-commit-config.yaml, matching the pattern
already used by airflow-core, task-sdk, and airflow-ctl. New .pre-commit-
config.yaml files are added to distributions that didn't have one. prek
auto-discovers nested configs, so the hooks remain part of the default
check set.

mypy-dev (covers dev + scripts), mypy-devel-common, and mypy-shared stay
at the repo root: dev/scripts/devel-common don't have their own configs,
and mypy-shared iterates every shared/<dist> distribution so has no
single home.

* Split mypy-dev and mypy-scripts, each with its own pyproject.toml config

Previously the mypy-dev prek hook ran mypy against dev/ and scripts/ in
a single invocation under the dev project's virtualenv. The two now get
independent hooks — mypy-dev in dev/.pre-commit-config.yaml and
mypy-scripts in scripts/.pre-commit-config.yaml — so each can evolve its
own dependency set and check its own folder.

Copy the full [tool.mypy] section from the root pyproject.toml into both
dev/pyproject.toml and scripts/pyproject.toml so each sub-project owns its
mypy configuration. Paths inside mypy_path are rewritten from
$MYPY_CONFIG_FILE_DIR/ to $MYPY_CONFIG_FILE_DIR/../ so they still resolve
to the repo-root siblings from the sub-project location. The decorator/
outputs plugins are scoped to dev only (scripts does not author DAG code).

mypy_local_folder.py now passes --config-file <project>/pyproject.toml
when the folder maps to one of these sub-project configs, so mypy uses
the sub-project's configuration rather than the root one.

* Teach selective-checks about the new non-provider mypy hooks

Add FileGroupForCi entries and regex patterns for helm-tests,
airflow-e2e-tests, docker-tests, kubernetes-tests, scripts, and shared
Python files, then wire them into skip_prek_hooks so the corresponding
mypy-* prek hook is only kept when its folder changed:

- mypy-scripts (split off from the old combined mypy-dev)
- mypy-airflow-ctl-tests, mypy-helm-tests, mypy-airflow-e2e-tests,
  mypy-task-sdk-integration-tests, mypy-docker-tests, mypy-kubernetes-tests
- mypy-shared

Update test_selective_checks.py skip-list constants and per-case inline
skip lists to include the new hooks. Targeted test cases for files under
the new-hook directories override skip-prek-hooks to leave the matching
hook out of the skip set, confirming it will run when its folder changes.

* Trim dev/scripts pyproject mypy_path to just relevant distributions

Drop the 200+ provider path entries that were blindly copied from the root
pyproject.toml. dev and scripts only import from other non-provider
workspace members, so listing every provider src/tests directory under
mypy_path just adds noise. The remaining non-provider entries cover
everything dev or scripts plausibly import from.

* Install mypy into per-hook venvs from uv.lock via a `mypy` dep group

Each non-provider distribution with a mypy prek hook now declares a
`mypy` dependency group in its pyproject.toml resolving to
`apache-airflow-devel-common[mypy]`. mypy_local_folder.py syncs each
dedicated virtualenv with `uv sync --frozen --project <X> --group mypy`
and runs mypy with `uv run --frozen --project <X> --group mypy` — so
mypy and its type stubs come from the workspace uv.lock, not from an
ephemeral `--with` overlay whose resolution is independent of the main
lockfile. uv.lock is refreshed to include the new group.

Covers airflow-core, task-sdk, airflow-ctl, devel-common, dev, scripts,
airflow-ctl-tests, helm-tests, airflow-e2e-tests, task-sdk-integration-
tests, docker-tests, kubernetes-tests, and every shared/<dist>
workspace member.

* Drop mypy_path from dev/scripts pyprojects — venv site-packages is enough

After the switch to installing mypy (and every transitive workspace
dependency) directly into each hook's virtualenv via the `mypy` dep
group, workspace packages like airflow, airflow.sdk, airflowctl,
airflow_breeze, tests_common are all available via the venv's
site-packages. mypy resolves them without needing mypy_path entries,
so drop the copied list and leave a short comment explaining why.

* Split mypy-shared into per-distribution hooks and enforce the pattern

Each shared/<dist> workspace member now owns a mypy-shared-<dist> prek hook
backed by its own shared/<dist>/.pre-commit-config.yaml. The single
mypy-shared iterator is gone — mypy_local_folder.py accepts shared/<dist>
as a first-class folder and the per-hook virtualenv now lives at
.build/mypy-venvs/shared-<dist>/ (slash in the folder name is replaced
with a dash in the venv/cache path).

Adds a new check-shared-mypy-hooks prek hook that fails when a
shared/<dist> workspace member is missing its dedicated .pre-commit-
config.yaml, printing the exact YAML to add. Selective-checks emits one
skip entry per dist, enumerated from shared/ at run time. Contributing
docs cover the two-step process for adding a new shared library.

* Pin minimum_prek_version to 0.3.4 consistently across all configs

All .pre-commit-config.yaml files now require prek >= 0.3.4 (the version
already declared by the root config). Previously the nested configs
pinned a mix of 0.2.0, 0.3.2, and 0.3.4, so a contributor could pass the
root's version check and still trip on stale subproject pins as they
moved between directories.

* Refresh uv.lock after rebase to reflect the `mypy` dep groups

The rebase onto main resolved the uv.lock conflict by taking main's
version, so `uv sync --group mypy` would fail against uv.lock until
the groups added to the per-distribution pyprojects were re-resolved.
Regenerates the lockfile to include them.

* Add explicit selective-checks test for per-shared-dist mypy hook skipping

Verifies that when a file under shared/logging/ changes, only
mypy-shared-logging is kept among the thirteen mypy-shared-* hooks;
all other shared distributions' hooks land in the skip list. Pins the
contract that the runtime enumeration over shared/*/pyproject.toml
works as intended.

* Refresh mypy docs to match the per-hook venv + --group mypy workflow

Fills in the docs that still referenced the pre-split workflow:

- AGENTS.md: mentions `mypy-shared-<dist>` per shared workspace member and
  the `uv sync --group mypy` install path for mypy itself.
- scripts/ci/prek/AGENTS.md: clarifies that non-provider mypy hooks run
  locally through mypy_local_folder.py (Breeze image only needed for the
  providers hook).
- dev/breeze/doc/03_developer_tasks.rst: renames stale `mypy-airflow` to
  `mypy-airflow-core`, and expands the cache note to cover the per-hook
  virtualenvs and caches under .build/.
- dev/breeze/doc/ci/04_selective_checks.md: expands the file-group and
  skip-reason lists so every new mypy hook (scripts, task-sdk, airflow-ctl,
  the six test-dir hooks, and mypy-shared-<dist> enumerated at runtime)
  is documented.

* Rename mypy_local_folder.py to run_mypy_full_dist_local_venv_or_breeze_in_ci.py

Updates every .pre-commit-config.yaml entry and prose references so they
point at the new script name. Two shared configs use YAML folded-scalar
entries to stay under the 110-char yamllint limit; updates the validation
script's expected template to match.
2026-04-20 13:04:32 +02:00

343 lines
13 KiB
ReStructuredText

.. Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
.. http://www.apache.org/licenses/LICENSE-2.0
.. Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
Static code checks
==================
The static code checks in Airflow are used to verify that the code meets certain quality standards.
All the static code checks can be run through prek hooks.
The prek hooks perform all the necessary installation when you run them
for the first time.
You can also run the checks via `Breeze <../dev/breeze/doc/README.rst>`_ environment.
.. contents:: Table of Contents
:depth: 2
:local:
Prek hooks
----------
Prek hooks help speed up your local development cycle and place less burden on the CI infrastructure.
Consider installing the prek hooks as a necessary prerequisite.
The hooks by default only check the files you are currently working on (and are staged) which makes the
checks rather fast. Yet, these checks use exactly the same environment as the CI tests
use. So, you can be sure your modifications will also work for CI if they pass
prek hooks.
We have integrated the `prek <https://github.com/j178/prek>`__ framework
in our development workflow. It can be installed in various ways and does not even need ``pip`` or
``python`` to be installed. It is a drop-in replacement for the legacy ``pre-commit`` tool, but it is
much faster and more feature-rich. It is written in Rust and it is designed to install environments in parallel,
so it is much faster than the ``pre-commit`` tool.
Installing prek hooks
---------------------
It is the best to use prek hooks when you have your local virtualenv for
Airflow activated since then prek hooks and other dependencies are
automatically installed. You can also install the prek hooks manually using ``uv`` or ``pipx``.
.. code-block:: bash
uv tool install prek
.. code-block:: bash
pipx install prek
Since we have a lot of hooks and sometimes you want to run them individually, it's advised to install
auto-completion for the ``prek`` command. You can do it by adding the following line to your
``.bashrc`` or ``.zshrc`` file:
For bash:
.. code-block:: bash
eval "$(COMPLETE=bash prek)" # for bash
For zsh:
.. code-block:: zsh
eval "$(COMPLETE=zsh prek)"
Similarly for other shells like fish, powershell, etc.
After installation, prek hooks are run automatically when you commit the code and they will
only run on the files that you change during your commit, so they are usually pretty fast and do
not slow down your iteration speed on your changes. There are also ways to disable the prek hooks
temporarily when you commit your code with ``--no-verify`` switch or skip certain checks that you find
to much disturbing your local workflow. See `Using prek <#using-prek>`_
The ``prek`` hooks use several external linters that need to be installed before prek is run.
Each of the checks installs its own environment, so you do not need to install those, but there are some
checks that require locally installed binaries. On Linux, you typically install
them with ``sudo apt install``, on macOS - with ``brew install``.
The current list of prerequisites is limited to ``xmllint`` and ``golang`` if you want to modify
the Golang code.:
- on Linux, install via ``sudo apt install libxml2-utils golang``
- on macOS, install via ``brew install libxml2 golang``
Some prek hooks also require the Docker Engine to be configured as the static
checks are executed in the Docker environment. You should build the images
locally before installing prek checks as described in `Breeze docs <../dev/breeze/doc/README.rst>`__.
Sometimes your image is outdated and needs to be rebuilt because some dependencies have been changed.
In such cases, the Docker-based prek will inform you that you should rebuild the image.
Enabling prek hooks
-------------------
To turn on prek checks for ``commit`` operations in git, enter:
.. code-block:: bash
prek install
To install the checks also for ``pre-push`` operations, enter:
.. code-block:: bash
prek install --hook-type pre-push
For details on advanced usage of the install method, use:
.. code-block:: bash
prek install --help
.. note::
The ``prek`` tool is a drop-in replacement for the legacy ``pre-commit`` tool - much faster and more
feature-rich, If you have already installed ``pre-commit`` to handle your hooks, you can run
``prek install -f`` to replace the existing ``pre-commit`` hooks with the ``prek`` hooks.
Available prek hooks
--------------------
You can see the list of available hooks by running:
.. code-block:: bash
prek list
You can also see more details about the hooks by running:
.. code-block:: bash
prek list --verbose
And if you want to see the details of a particular hook, you can run:
.. code-block:: bash
prek list --verbose <hook-id>
When you install auto-completion, you can also use the tab-completion to see the available hooks.
Using prek
----------
After installation, prek hooks are run automatically when you commit the
code or push it to the repository (depending on stages configured for the hooks). Some of the
hooks are configured to run on "manual" stage only and are not run automatically.
By default when you run ``prek``, the ``pre-commit`` stage hooks are run.
But you can run prek hooks manually as needed.
- Run all checks on your staged files by using:
.. code-block:: bash
prek
- Run only mypy check on your staged airflow and dev files by specifying the
``mypy-airflow-core`` and ``mypy-dev`` prek hooks (more hooks can be specified).
For non-provider projects, mypy runs locally via ``uv`` (no breeze image needed):
.. code-block:: bash
prek mypy-airflow-core mypy-dev
- Run only mypy airflow checks on all "airflow-core" files by using:
.. code-block:: bash
prek mypy-airflow-core --all-files
- Run all pre-commit stage hooks on all files by using:
.. code-block:: bash
prek --all-files
- Run all pre-commit stage hooks only on files modified in the last locally available
commit in your checked out branch:
.. code-block:: bash
prek --last-commit
- Run all pre-commit stage hooks only on files modified in your last branch that is targeted
to be merged into the main branch:
.. code-block:: bash
prek --from-ref main
- Show files modified automatically by prek when prek automatically fixes errors (after running all
``pre-commit`` stage hooks on locally modified files):
.. code-block:: bash
prek --show-diff-on-failure
- Skip one or more of the checks by specifying a comma-separated list of
checks to skip in the SKIP variable:
.. code-block:: bash
SKIP=ruff,rst-backticks prek --all-files
You can always skip running the tests by providing ``--no-verify`` flag to the
``git commit`` command.
To check other usage types of the pre-commit framework, see `Pre-commit website <https://pre-commit.com/>`__.
Disabling particular checks
---------------------------
In case you have a problem with running particular ``prek`` check you can still continue using the
benefits of having ``prek`` installed, with some of the checks disabled. In order to disable
checks you might need to set ``SKIP`` environment variable to coma-separated list of checks to skip. For example,
when you want to skip some checks (ruff/mypy for example), you should be able to do it by setting
``export SKIP=ruff,mypy-airflow-core,``. You can also add this to your ``.bashrc`` or ``.zshrc`` if you
do not want to set it manually every time you enter the terminal.
In case you do not have breeze image configured locally, you can also disable all checks that require breeze
the image by setting ``SKIP_BREEZE_PREK_HOOKS`` to "true". This will mark the tests as "green" automatically
when run locally (note that those checks will anyway run in CI).
Disabling goproxy for firewall issues
-------------------------------------
Sometimes your environment might not allow to connect to the ``goproxy`` server, which is used to
proxy/cache Go modules. When your firewall blocks go proxy it usually ends with message similar to:
.. code-block:: text
lookup proxy.golang.org: i/o timeout
In such case, you can disable the ``goproxy`` by setting the
``GOPROXY`` environment variable to "direct". You can do it by running:
.. code-block:: bash
export GOPROXY=direct
Alternatively if your company has its own Go proxy, you can set the ``GOPROXY`` to
your company Go proxy URL. For example:
.. code-block:: bash
export GOPROXY=https://mycompanygoproxy.com
See `Go Proxy lesson <https://www.practical-go-lessons.com/chap-18-go-module-proxies#configuration-of-the-go-module-proxy>`__)
for more details on how to configure Go proxy - including setting multiple proxies.
You can add the variable to your ``.bashrc`` or ``.zshrc`` if you do not want to set it manually every time you
enter the terminal.
Manual prek hooks
-----------------
Most of the checks we run are configured to run automatically when you commit the code or push PR. However,
there are some checks that are not run automatically and you need to run them manually. You can run
them manually by running ``prek --stage manual <hook-id>``.
Mypy checks
-----------
When we run mypy checks locally, the ``mypy-*`` checks run depending on the files you are changing:
``mypy-airflow-core``, ``mypy-dev``, ``mypy-providers``, ``mypy-scripts``, ``mypy-task-sdk``,
``mypy-airflow-ctl``, ``mypy-devel-common``, ``mypy-airflow-ctl-tests``, ``mypy-helm-tests``,
``mypy-airflow-e2e-tests``, ``mypy-task-sdk-integration-tests``, ``mypy-docker-tests``,
``mypy-kubernetes-tests``, and one ``mypy-shared-<dist>`` hook per ``shared/<dist>`` workspace
distribution (e.g. ``mypy-shared-configuration``, ``mypy-shared-logging``).
For **non-provider projects**, mypy runs locally using ``uv`` — no breeze CI image is needed. These
checks run as regular prek hooks in the ``pre-commit`` stage, checking whole directories at once. This
means they run both as part of local commits and as part of regular static checks in CI (not as
separate mypy CI jobs).
Each non-provider ``mypy-*`` hook uses a **dedicated virtualenv and mypy cache** under ``.build/`` so
running mypy never mutates your regular project ``.venv`` and each hook keeps a stable, CI-aligned
dependency set:
- virtualenvs: ``.build/mypy-venvs/<hook-name>/``
- mypy caches: ``.build/mypy-caches/<hook-name>/``
Adding a new shared library
~~~~~~~~~~~~~~~~~~~~~~~~~~~
Every ``shared/<dist>`` workspace member has its own ``mypy-shared-<dist>`` prek hook so it is
type-checked in isolation against its own dependency set. When you add a new shared library under
``shared/<new-dist>/``, you also need to:
1. Add a ``[dependency-groups]`` section with ``mypy = ["apache-airflow-devel-common[mypy]"]`` in
``shared/<new-dist>/pyproject.toml`` (so ``uv sync --group mypy`` installs mypy into the hook's
dedicated virtualenv).
2. Create ``shared/<new-dist>/.pre-commit-config.yaml`` with a ``mypy-shared-<new-dist>`` hook
entry that calls ``../../scripts/ci/prek/run_mypy_full_dist_local_venv_or_breeze_in_ci.py shared/<new-dist>``.
The ``check-shared-mypy-hooks`` prek hook enforces step 2 — it fails and prints the exact config
contents to add when any ``shared/<dist>`` is missing its dedicated mypy hook.
To run the prek hook for a specific project (example for ``airflow-core`` files):
.. code-block:: bash
prek mypy-airflow-core --all-files
To show unused mypy ignores, run:
.. code-block:: bash
export SHOW_UNUSED_MYPY_WARNINGS=true
prek mypy-airflow-core --all-files
For **providers**, mypy still runs via breeze (``breeze run mypy``) as a separate CI job and requires
``breeze ci-image build --python 3.10`` to be built locally. Providers use a separate docker-volume
(called ``mypy-cache-volume``) that keeps the cache of last MyPy execution.
To clear all mypy caches (the Docker volume used by providers, any legacy repo-root ``.mypy_cache``,
and the per-hook venvs + caches under ``.build/mypy-venvs/`` and ``.build/mypy-caches/``), run
``breeze down --cleanup-mypy-cache``.
-----------
Once your code passes all the static code checks, you should take a look at `Testing documentation <09_testing.rst>`__
to learn about various ways to test the code.