Skip to content

build(deps): bump pytest to 9.0.3 - #32

Open
ocelotl wants to merge 7 commits into
mainfrom
pytest9_upgrade
Open

ocelotl wants to merge 7 commits into
mainfrom
pytest9_upgrade

Conversation

@ocelotl

@ocelotl ocelotl commented Nov 1, 2026 •

Copy link
Copy Markdown
Owner

Raises pytest to 9.0.3 across every requirements file in the repository, which is the fix for GHSA-6w46-j5rx-g56g, and carries the two changes that are required for the test suites to pass under pytest 9.

Definitions

  • A generated requirements file is a file whose contents uv pip compile produces from a sibling .in file, rather than a file edited by hand. In this repository such a file is named test-requirements.oldest.txt or test-requirements.latest.txt and is compiled from test-requirements.in. For example, instrumentation/opentelemetry-instrumentation-fastapi/test-requirements.oldest.txt is compiled from instrumentation/opentelemetry-instrumentation-fastapi/test-requirements.in with --resolution lowest-direct.

  • A lint environment is a tox environment whose name begins with lint- and which installs dev-requirements.txt together with the test-requirements.txt of one package, resolving both in a single uv pip install invocation. Because the two files are resolved together, a version of pytest pinned in one of them and a different version pinned in the other make the environment unsatisfiable.

  • A floor is the lowest version of a distribution that a requirement permits. For example, the requirement pytest~=9.0 in a test-requirements.in sets the floor of pytest to 9.0, and --resolution lowest-direct then selects exactly that floor when compiling test-requirements.oldest.txt.

What this closes

60 open Dependabot alerts, all of them instances of the same advisory, GHSA-6w46-j5rx-g56g (GHSA-6w46-j5rx-g56g), severity medium, titled "pytest has vulnerable tmpdir handling". Its vulnerable range is < 9.0.3 and its first patched version is 9.0.3, so there is no version below 9.0.3 that clears it. Dependabot raises one alert per manifest file, which is why a single advisory accounts for 60 alerts in a repository that pins pytest in 121 files.

Why the bump has to be atomic

Every file that pins pytest moves in this one pull request, and the bump cannot be split across several smaller ones. A lint environment installs dev-requirements.txt and one package's test-requirements.txt in a single resolution, so as soon as those two files disagree the resolver fails with:

error: Because you require pytest==9.0.3 and pytest==7.4.4, we can conclude that your requirements are unsatisfiable

This was confirmed by trying the split first: moving pytest in a subset of the files turned every lint- job red.

The seven commits

build(deps): bump flaky to 3.8.1 is first because pytest 9.0.3 cannot be installed alongside flaky 3.7.0. flaky 3.7.0 imports a name that pytest 9 removed, and collection aborts with:

ImportError: cannot import name 'call_runtest_hook' from '_pytest.runner'

flaky 3.8.1 works under both pytest 7.4.4 and pytest 9.0.3, so this commit is green on its own. It touches dev-requirements.txt, exporter/opentelemetry-exporter-richconsole/test-requirements.txt and tests/opentelemetry-docker-tests/tests/test-requirements.txt.

fix(grpc): make the generated protobuf test directory an importable package adds an empty instrumentation/opentelemetry-instrumentation-grpc/tests/protobuf/__init__.py. pytest 9 defaults to the importlib import mode, under which a directory with no __init__.py is not a package, and the absolute import at instrumentation/opentelemetry-instrumentation-grpc/tests/protobuf/test_server_pb2_grpc.py:3, written from tests.protobuf import test_server_pb2 as test__server__pb2, then fails at collection. The file carries only the licence header, matching the sibling instrumentation/opentelemetry-instrumentation-grpc/tests/__init__.py.

build(deps): bump pytest to 9.0.3 is the bump itself, across 121 files. It also raises pytest-asyncio from 0.23.5 to 1.4.1 in three files, because pytest-asyncio 0.23.5 declares pytest<9 and 1.4.1 declares pytest<10,>=8.4. The three files are instrumentation/opentelemetry-instrumentation-aiohttp-server/test-requirements.txt, instrumentation/opentelemetry-instrumentation-asyncio/test-requirements.txt and instrumentation/opentelemetry-instrumentation-click/test-requirements.txt.

Six test-requirements.in files have their pytest floor raised to ~=9.0, and their generated requirements files are recompiled with uv 0.13.6 rather than hand-edited. Editing only the pytest line of an oldest file leaves packaging pinned below 22, and pytest 9 requires packaging>=22.0, so the environment fails to install with:

Because pytest==9.0.3 depends on packaging>=22 and you require packaging==18.0, we can conclude that your requirements are unsatisfiable

The six packages are aio-pika, asyncclick, kafka-python, fastapi, sqlalchemy and structlog.

build(deps): bump syrupy to 6.1.1 is required for the same reason as the flaky bump: syrupy 4.6.1 declares pytest>=7.0.0,<9.0.0, so the two resourcedetector-gcp environments cannot be installed at all under pytest 9.0.3. syrupy 5.0.0 is the lowest release that permits pytest 9 and 6.1.1 is the highest. The 31 stored snapshots under resource/opentelemetry-resourcedetector-gcp/tests/__snapshots__ match unchanged under both, so this takes 6.1.1 and keeps resource/opentelemetry-resourcedetector-gcp/requirements.oldest.txt and resource/opentelemetry-resourcedetector-gcp/requirements.latest.txt identical outside their opentelemetry-api and opentelemetry-sdk pins, which is the only difference between those two files today.

fix(botocore): raise pytest.xfail.Exception instead of calling pytest.xfail changes 11 call sites in instrumentation/opentelemetry-instrumentation-botocore/tests/test_botocore_bedrock.py. In pytest 9, pytest.xfail is an instance of _pytest.outcomes._XFail rather than a function. Its __call__ is still annotated -> NoReturn, but pylint does not follow that annotation through __call__, so every variable assigned only in the branches of an if/elif chain whose else branch calls pytest.xfail is reported as possibly-used-before-assignment:

tests/test_botocore_bedrock.py:1464:25: E0606: Possibly using variable 'finish_reason' before assignment (possibly-used-before-assignment)

That accounts for 27 errors in that one file and turns lint-instrumentation-botocore red. pytest.xfail.Exception is _pytest.outcomes.XFailed in both pytest 7.4.4 and pytest 9.0.3, and _XFail.__call__ does nothing but raise it, so raising it directly is equivalent and pylint recognises the branch as terminating. This was checked against pylint 4.0.9 with both versions of pytest installed in turn.

test(tornado): widen the concurrent-request duration tolerance to 50ms changes one argument in instrumentation/opentelemetry-instrumentation-tornado/tests/test_metrics_instrumentation.py. test_metrics_concurrent_requests issues a 1 second request to /slow and a fast request to /async at the same time, then requires the client-measured and server-measured durations of the slow one to agree within 10 ms. On the GitHub Python 3.10 runners that assertion now lands between 21 ms and 37 ms, and the whole excess is on the client side: one failing run reported server_ms=1002 against client_s=1.0242199897766113, while a passing run in the same runner region reported server_ms=1003 against client_s=1.005490779876709. The client figure covers connection setup and the event loop dispatch of two concurrent fetches, neither of which the server figure includes, and the server figure is quantised to whole milliseconds on top of that.

I could not attribute the change to pytest 9 itself. A branch carrying nothing but pytest 9.0.3 for instrumentation/opentelemetry-instrumentation-tornado/test-requirements.txt, on top of the same base commit, passes this test with a measured gap of 2.5 ms, and the tornado environment resolves to the same packages on both branches. 10 ms of a 1 second measurement is 1 percent, which is below the noise floor of a shared runner. The assertion exists to catch per-request state being mixed up between concurrent requests, and that failure mode substitutes a 1 ms duration for a 1000 ms one, so it is still caught with a far wider tolerance. 50 ms is what this same test already allows for PyPy.

test(tornado): widen the second concurrent-request tolerance as well extends the same change to the /async request of the pair, whose assertion then failed at 0.021571563720703124 against its own 20 ms tolerance. The two assertions that follow it compare only server-side values, 1002 ms for /slow and 1 ms to 3 ms for /async, against a tolerance of 100 ms, so client-side time does not reach them and they are left alone.

The one generated requirements file that was not regenerated

instrumentation/opentelemetry-instrumentation-fastapi/test-requirements.latest.txt is edited by hand, changing the pytest line and nothing else. This is the single deliberate exception to recompiling every generated requirements file that the previous section describes.

Recompiling it moves starlette from 1.3.1 down to 0.48.0, because instrumentation/opentelemetry-instrumentation-fastapi/test-requirements.in carries fastapi~=0.119.1 and that range of fastapi caps starlette below 1.0. starlette 0.48.0 is inside the vulnerable range of GHSA-82w8-qh3p-5jfq, so recompiling would reopen six starlette alerts while closing pytest ones, which is a net loss.

Raising the fastapi floor far enough to keep starlette at 1.3.1 contradicts the declared support range at instrumentation/opentelemetry-instrumentation-fastapi/pyproject.toml:37, which reads "fastapi ~= 0.92". That is a decision about which versions of fastapi this instrumentation supports, not a dependency bump, so it is left out of this pull request.

Verification

24 test suites were run locally on Python 3.12 against the branch, all passing: richconsole 4, grpc 156 twice, aiohttp-server 37, asyncio 24, click 8, asyncclick 13 twice, aio-pika 9, kafka-python 14 twice, fastapi 109 twice, asgi 105, django 83, flask 64, falcon 45, httpx 179, celery 35, botocore 150, aiohttp-client 48, sqlalchemy 68 twice and structlog 36 twice.

A scan over every requirements file in the branch confirms that no file pins a version of packaging below 22 alongside pytest 9.0.3.

flaky 3.7.0 does `from _pytest.runner import call_runtest_hook`, which pytest
removed. Under pytest 9 the import fails and every environment that installs
flaky dies at collection.

3.8.1 works under both pytest 7.4.4 and pytest 9.0.3, so the bump is safe ahead
of the pytest upgrade and is kept separate from it.
…ackage

instrumentation/opentelemetry-instrumentation-grpc/tests/protobuf/test_server_pb2_grpc.py:3
does `from tests.protobuf import test_server_pb2`, an absolute import that only
resolves while the package directory is on sys.path and `tests.protobuf` is a
real package. The directory had no __init__.py, so resolution depended on
pytest inserting the directory itself, which pytest 9 no longer does by default.

Adding the missing __init__.py makes the import work without relying on an
import-mode default. The grpc suite goes from 12 collection errors to 156
passed.
GHSA-6w46-j5rx-g56g has no fix below pytest 9.0.3 and affects 60 open alerts.

The bump has to cover every file at once. The lint environments install
dev-requirements.txt and the package's own test-requirements.txt in a single
resolution, so any file left on 7.4.4 makes the lint environment for its
package unresolvable. 115 files pin pytest, not the 60 that carry an alert.

Three things move with pytest because pytest 9 requires them:

- pytest-asyncio 0.23.5 to 1.4.1 in the aiohttp-server, asyncio and click test
  requirements, since 0.23.5 caps pytest below 9. No test or conftest change was
  needed. The six instrumentation-genai files keep pytest-asyncio 0.21.0, which
  works unchanged under pytest 9.
- The uv-generated requirements files are regenerated rather than hand-edited,
  because pytest 9 requires packaging>=22.0 and the oldest variants resolved
  packaging to 18.0 or 21.0, which cannot satisfy it. Regeneration also adds
  pygments, which pytest 9 depends on and pytest 7.4.4 did not, and drops
  pyparsing and six, which packaging 22.0 no longer needs.
- The pytest floor in each regenerated .in file moves from 7.4 to 9.0.

instrumentation/opentelemetry-instrumentation-fastapi/test-requirements.latest.txt
is edited by hand instead of regenerated. Regenerating it from the current .in
replaces fastapi 0.137.2 with 0.119.1 and starlette 1.3.1 with 0.48.0, because
the .in still asks for fastapi~=0.119.1 and fastapi 0.119.1 caps starlette below
0.49.0. Starlette's first fixed release is 0.49.1, so regenerating would reopen
six starlette advisories that are currently closed. The .in and the generated
file have drifted and need reconciling in their own change.
@ocelotl ocelotl added the Skip Changelog Do not require a changelog fragment label Nov 1, 2026
@github-actions github-actions Bot added the gen-ai label Nov 1, 2026
syrupy 4.6.1 declares pytest>=7.0.0,<9.0.0, so the resourcedetector-gcp
environments cannot be installed alongside pytest 9.0.3:

  Because syrupy==4.6.1 depends on pytest>=7.0.0,<9.0.0 and you require
  pytest==9.0.3, we can conclude that your requirements and syrupy==4.6.1
  are incompatible.

syrupy 5.0.0 is the lowest release that permits pytest 9. 6.1.1 is the
highest. The 31 stored snapshots under
resource/opentelemetry-resourcedetector-gcp/tests/__snapshots__ match
unchanged under both, so this takes the highest and keeps the two
requirements files identical outside their opentelemetry-api and
opentelemetry-sdk pins.
….xfail

In pytest 9, pytest.xfail is an instance of _pytest.outcomes._XFail rather
than a function. Its __call__ is still annotated -> NoReturn, but pylint
does not follow the annotation through __call__, so every variable assigned
only in the branches of an if/elif chain whose else branch calls
pytest.xfail is reported as possibly-used-before-assignment:

  tests/test_botocore_bedrock.py:1464:25: E0606: Possibly using variable
  'finish_reason' before assignment (possibly-used-before-assignment)

That accounts for 27 errors in this file and fails lint-instrumentation-botocore.

pytest.xfail.Exception is _pytest.outcomes.XFailed in both pytest 7.4.4 and
pytest 9.0.3, and _XFail.__call__ does nothing but raise it, so raising it
directly is equivalent and pylint recognises the branch as terminating.
Verified against pylint 4.0.9 with both pytest versions.
test_metrics_concurrent_requests issues a 1 second request to /slow and a
fast request to /async at the same time, then requires the client-measured
and server-measured durations of the slow one to agree within 10ms.

On the GitHub Python 3.10 runners that assertion now lands between 21ms and
37ms, and the whole excess is on the client side. One failing run reported
server_ms=1002 against client_s=1.0242199897766113, while a passing run on
the same runner region reported server_ms=1003 against
client_s=1.005490779876709. The client figure covers connection setup and
the event loop dispatch of two concurrent fetches, neither of which the
server figure includes, and the server figure is additionally quantised to
whole milliseconds.

10ms of a 1 second measurement is 1 percent, which is below the noise floor
of a shared runner. The purpose of the assertion is to catch per-request
state being mixed up between concurrent requests, and that failure mode
swaps a 1000ms duration for a 1ms one, so it stays caught with a far wider
tolerance. 50ms is already what this same test allows for PyPy.
The previous commit widened the tolerance for the /slow request only. The
client-side excess it describes applies to both requests of the pair, so
the /async request now fails the same way:

  AssertionError: 0.021571563720703124 != 0.0 within 0.02 delta

The reasoning is the one given in the previous commit, and 50ms is again
what this test already allows for PyPy. The two assertions that follow
compare only server-side values, 1002ms for /slow and 1ms to 3ms for
/async, against a tolerance of 100ms, so they are unaffected by client-side
time and are left alone.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

gen-ai Skip Changelog Do not require a changelog fragment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant