Repository navigation
Description
Twelve of the eighteen direct runtime requirements in [project.optional-dependencies]
declare no floor at all. Six do — litestar>=2.15, prometheus-fastapi-instrumentator>=8.0.1,
prometheus-client>=0.20, opentelemetry-instrumentation-asgi>=0.46b0, faststream>=0.6,
fastmcp>=3.0 — and the rest are bare:
- core:
typing-extensions orjson,sentry-sdk,pyroscope-io,structlog,fastapiopentelemetry-api,opentelemetry-sdk,opentelemetry-instrumentation,
opentelemetry-exporter-otlp-proto-grpc,opentelemetry-exporter-otlp-proto-http,
opentelemetry-instrumentation-fastapi
A bare requirement is not a wider range than a floored one; it is a claim that every release
back to the first one works. uv pip install --resolution lowest-direct reads it that way and
resolves fastapi==0.1.0, structlog==0.1.0, sentry-sdk==0.1.0, opentelemetry-{api,sdk}==1.0.0
with opentelemetry-instrumentation==0.19b0, pyroscope-io==0.3.0, orjson==2.0.7.
Core is the case that settles it, because it needs no extra to reproduce. A bare install at
lowest-direct resolves typing-extensions==3.6.2 and does not import:
ImportError: cannot import name 'GenericMeta' from 'typing'
at lite_bootstrap/instruments/base.py:4
typing_extensions.Self (BaseConfig.from_dict/from_object) landed in 4.0.0, and 4.0.0 imports
clean. ADR-0007
argues that dependency is load-bearing and names both uses; the floor those uses imply was never
written down. That is a real undeclared minimum today, independent of any CI work.
Establish the lowest version each bare requirement is actually tested against and declare it, the
way litestar>=2.15 names the two APIs that forced it and #211 established faststream>=0.6 and
fastmcp>=3.0 release by release. A floor gets a comment when a specific API forced it, and none
when it is simply the oldest release the suite passes on.
Two things to decide while doing it:
- Whether a floor is derived from the library or from the suite.
tests/conftest.pyimports
sentry_sdk.envelope.Envelopeandstructlog.typingat module level, and the FastStream tests
importfaststream._internal.logger.params_storage.ManualLoggerStorage. A floor established by
running the suite is therefore at least as high as the test helpers need, which can exceed the
library's own minimum and over-constrains downstream services. Thelitestarandfastmcp
floors were derived from library APIs; picking one rule and applying it to all twelve keeps the
set coherent. - The dev and lint groups. They are unfloored too (
redis>=5.2.1is the only exception), which
is whyuv lock --resolution lowest-directfails outright:pytestresolves to 2.0.0 and its
sdist 403s on a 2011 setuptools bootstrap URL.uv lockhas no--no-devor
--no-default-groups, so the lock form of Exercise declared dependency floors in CI #210 needs floors here as well, while the per-extra
uv pip installform does not. Worth settling alongside, since it decides which form Exercise declared dependency floors in CI #210 can use.
#210 is the job that holds these floors once they exist; it cannot go green before this lands.
Note that nothing enforces a floor at runtime either: import_checker.py is presence-only
(find_spec), so a too-old-but-present dependency passes every guard and fails at attribute access
inside bootstrap() rather than as InstrumentDependencyMissingWarning. The declaration is the
only defence, which is what makes an unwritten one costly.
Revisit trigger: a new call into one of these packages from an instrument or bootstrapper, which
raises the real minimum whether or not the declared floor moves with it.
Activity
lesnik512 commentedon Sep 13, 2026
Taking the first open decision, since it sets the method for the remaining eleven and is cheap to
settle before any of them are bisected.
The gap is real, and it is measurable
structlogis the case that shows it. Same repo, same interpreter (3.12), two oracles:[logging]installed, bootstrap aFreeBootstrapperand emit a linestructlog.stdlib.ProcessorFormatter.remove_processors_meta(logging_instrument.py:158)tests/instruments/test_logging_instrument.py+test_structured_log_payload.pystructlog.typing, imported at runtime bytests/conftest.py:11Five releases and about eleven months apart. The library imports
structlog.typingonly underif typing.TYPE_CHECKING:(logging_instrument.py:19), so nothing inlite_bootstrapneeds thatmodule to exist at runtime. A suite-derived floor would publish
structlog>=22.2and refuse anenvironment the library runs on perfectly well.
The same shape appears wherever a test reaches for something the library does not:
sentry_sdk.envelope.Envelopeandsentry_sdk.Transport(conftest builds a capture transport; thelibrary only calls
init,set_tags,flush),faststream._internal.broker.BrokerUsecaseandfaststream.redis.{RedisBroker,TestRedisBroker},opentelemetry.sdk.trace.export.in_memory_span_exporter,starlette.testclient.TestClientandlitestar.testing.TestClient.Not all of those bite. The divergence only matters where the test reach is newer than the library
reach, which is why it has to be measured per package rather than assumed.
structlogis the onemeasured instance so far.
Proposal: floors are library-derived
A declared floor is a promise to someone who installs
lite-bootstrap[logging]and never sees thisrepo's tests. Deriving it from the suite makes every test helper a downstream constraint, silently,
and the constraint moves whenever a test is rewritten.
ManualLoggerStorageis the useful contrast:it looks like a test detail but
faststream_bootstrapper.py:17imports it at runtime, so it islibrary-derived and genuinely forced
faststream>=0.6in #211.Concretely:
typing-extensionsinfeat: declare minimum compatible version for typing-extensions #214 that was bare import, because the two uses are reached at import time. For a framework extra
it is constructing that bootstrapper and calling
bootstrap()/teardown().structlog.typingin
conftest.pycan move behindtyping.TYPE_CHECKINGexactly as the library has it, whichcollapses this particular gap to nothing.
A test client or an envelope inspector is not something a downstream service installs. Putting
structlog>=22.2in[dependency-groups] devwhile the extra declares>=21.3states both factsaccurately, and a downstream user never installs the dev group. It also gives the dev group the
floors the lock form of Exercise declared dependency floors in CI #210 needs anyway.
Cost, stated plainly
This decision has a consequence for #210. If floors are library-derived but the CI job runs the full
pytest suite under
--resolution lowest-direct, the job fails on the declared floor whenever the twodiverge: 21.3.0 resolves, then
conftest.pycannot import. So either the residual divergence iscarried in the dev group as above, which keeps the suite installable while the extra stays honest,
or #210's lowest-direct oracle is a per-framework bootstrap smoke rather than pytest.
The dev-group route is the better one. It keeps one oracle for the declaration and one for CI without
them contradicting each other, and the smoke-test alternative would be a second, weaker test path to
maintain alongside the suite.
What this settles
Derive every remaining floor from library API use. Measure it by bootstrapping, not by running the
suite. Carry any test-only excess in
[dependency-groups] dev.If that reads right, the OTel cluster is the next piece of work: six of the eleven, and they share a
release train, so they want one decision rather than six.
lesnik512 commentedon Sep 15, 2026
Addendum to the method, before the remaining four are swept in one pass.
Where no API forces a floor
The rule settled above derives a floor from library API use. Two of the four remaining requirements
have no such use to derive from:
orjson:orjson.dumps(value, **kwargs).decode()andorjson.loads(logging_factory.py:40,51).Both have existed since 1.x.
pyroscope-io:configure,shutdown,add_thread_tag,remove_thread_tag. All original API.Reading the rule literally would floor them at their first release, which is exactly the claim #210
exists to stop anyone making. What actually fails there is installation, not the call:
pyroscope-io0.3.0 ships an sdist whose
pyproject.tomlhas noproject.name, and no current build backendaccepts it. That is the error already blocking
[fastapi-all]underlowest-directtoday.So, extending the rule by one clause: where no API use forces a floor, the floor is the lowest
release that installs and imports across the supported interpreter matrix, and the comment says that
is the reason. A floor with a packaging rationale reads differently from one that names a symbol,
and it should, because a later release of the same package could legitimately move it.
This is not new ground. #215 already landed a floor of this shape: the OpenTelemetry train sits at
1.28 / 0.49b0 not because
lite-bootstrapreaches for anything newer, but becauseopentelemetry-instrumentationbelow 0.49b0 importspkg_resourcesunder a stalesetuptools>=16.0declaration and no longer installs. The clause names what that PR already did.
structlogand the conftest importThe
structlog.typingdivergence measured above (21.3.0 library, 22.2.0 suite) resolves to thedev-group route, not to fixing the test.
tests/conftest.py:11importsEventDictandWrappedLoggerfor annotations only, so atyping.TYPE_CHECKINGguard would work mechanically. But[tool.ruff.lint]ignoresTCHrepo-wide,on the stated grounds that "imports stay real; TYPE_CHECKING-only imports break runtime
introspection". Rewriting the test to satisfy a floor would cut against that, for a package where the
library genuinely does not need the module at runtime.
So:
structlog>=21.3in theloggingextra,structlog>=22.2in[dependency-groups] dev. Theextra states what a downstream service needs; the dev group states what running this repo's suite
needs. Nobody installs the second.
Scope of the sweep
The four runtime requirements, in one PR:
structlog,sentry-sdk,orjson,pyroscope-io.The seven bare entries in
[dependency-groups]stay out. They matter only if #210 choosesuv lock --resolution lowest-direct, and the comment on that issue argues for the per-extrauv pip installform instead. Declaring them now would be pre-deciding that.