Python package dependency checking in a CI pipeline with pipdeptree
Posted on Sun 26 June 2022 in hints-and-kinks • 3 min read
Recently at work we ran into rather strange-looking errors that broke some functionality we depend on.
In an application run from a CI-built container image, we were seeing
pkg_resources.ContextualVersionConflict errors indicating that one of
our packages could not find a matching installed version of protobuf.
Specifically, that package wanted protobuf<4 installed, but the
installed version of the protobuf package was 4.21.1.
This was somewhat puzzling: all Python packages in the image were
installed with pip, and the packages’ requirements ought to have been
in good shape.
We found another dependency that did specify protobuf<5, but taken
together pip should surely resolve that into a 3.x version of
protobuf, in order to satisfy both the protobuf<4 requirement from
one package, and the protobuf<5 one from another?
To visualize and test such dependencies, the pipdeptree utility comes
in quite handy.
So, I hacked up a couple of minimal tox testenvs:
[testenv:pipdeptree]
deps =
pipdeptree
commands = pipdeptree -w fail
[testenv:pipdeptree-requirements]
deps =
-rrequirements.txt
pipdeptree
commands = pipdeptree -w fail
The first one, pipdeptree, merely installs the package being built,
obeying the install_requires list in its setup.py file. This is the
“minimal” installation.
The second one, pipdeptree-requirements, runs a full installation,
pulling in everything needed from the requirements.txt file.
pipdeptree generates warnings on potential version conflicts between
dependent packages. So, in both testenvs, we run pipdeptree in
-w fail mode, which turns all warnings into errors that fail the
testenv.
So now, having added tox to both our CI and our local Git hooks, we can run these checks locally and from GitHub Actions, and they should both fail and thereby expose our package dependency bug, right?
Well, here is where it got weird.
Because if I ran that locally, on my Ubuntu Focal development laptop, I got:
- protobuf [required: >=3.15.0,<4.0.0dev, installed: 4.21.1]
- protobuf [required: >=3.15.0,<5.0.0dev, installed: 4.21.1]
This is “bad” in the sense that it’s the wrong protobuf version, but
good in that it exposes the bug that we’re trying to fix. Progress!
However, running the same thing from our GitHub Actions workflow, there’s this:
- protobuf [required: >=3.15.0,<4.0.0dev, installed: 3.20.1]
- protobuf [required: >=3.15.0,<5.0.0dev, installed: 3.20.1]
So here, in GitHub Actions, we see a protobuf version being installed
that doesn’t break anything, but it also means that our test doesn’t
expose our bug, which is a problem!
I’ll spare you the details of finding this out, but it turned out that
this is actually a pip problem. pip 20.0.2 (which is the version you
get when you run apt install python3-pip on Ubuntu Focal) has the
dependency resolution error, which results in a protobuf package that
is “too new”. If you install with pip version 21 or later, you get a
protobuf that is “old enough” to make all installed packages happy.
So, how do we test that?
There is a package called
tox-pip-version that
comes in very handy here, in that it allows you to set an environment
variable, TOX_PIP_VERSION, instructing tox what pip version it
should use in order to install packages into testenvs.
This you can use from a GitHub Actions jobs definition, making use of
a matrix strategy:
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
python-version:
- 3.8
- 3.9
pip-version:
- 20.0.2
- 22.0.4
steps:
- name: Check out code
uses: actions/checkout@v1
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v2
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
pip install tox tox-gh-actions tox-pip-version
- env:
TOX_PIP_VERSION: ${{ matrix.pip-version }}
name: Test with tox (pip ${{ matrix.pip-version }})
run: tox
What this does is it sets up a 2×2 matrix: run with Python 3.8 and
Python 3.9, and for both those Python versions run with pip 20.0.2 and
22.0.4 (these happen to be the two versions that we’re interested in).
That way, we were able to expose the package dependency bug, and then fix it. The test now serves as a regression test, to make sure we don’t run into a similar issue again.
If you’re curious, the full PR discussion with additional context is on GitHub.