Skip to content

Commit 56ba454

Browse files
lesnik512claude
andauthored
ci: isolation install-check + fix lite-bootstrap[litestar] import (#159)
* docs(planning): spec per-extra isolation install-check + litestar fix Add a CI job installing each extra in isolation and importing lite_bootstrap (the --all-extras suite masks undeclared transitive deps), and fix the litestar bug it found: litestar.plugins.prometheus is imported under is_litestar_installed but needs prometheus_client, so lite-bootstrap[litestar] crashed on import. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(litestar): guard the prometheus plugin import behind prometheus_client litestar_bootstrapper imported litestar.plugins.prometheus (which imports prometheus_client) under the is_litestar_installed guard, but the `litestar` extra does not install prometheus-client — only `litestar-metrics` does. So `pip install lite-bootstrap[litestar]` + `import lite_bootstrap` crashed with litestar's MissingDependencyException. Guard the import on prometheus_client too; its only uses are inside the metrics instrument, already gated on that package. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * ci: install each extra in isolation and import lite_bootstrap The existing suite only installs --all-extras, so an extra that imports an undeclared package passes CI while breaking a real single-extra install (shipped twice: the bare-core typing_extensions crash and litestar/prometheus). Add a job that, per extra, installs .[extra] into a clean venv and imports lite_bootstrap, failing with the full list of any that break. Python 3.10 (the floor). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 65845e0 commit 56ba454

5 files changed

Lines changed: 199 additions & 3 deletions

File tree

‎.github/workflows/_checks.yml‎

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,3 +69,38 @@ jobs:
6969
run: uv pip install ".[${{ matrix.extras }}]"
7070
- name: Free-threaded smoke test
7171
run: .venv/bin/python scripts/ft_smoke.py
72+
73+
install-isolation:
74+
runs-on: ubuntu-latest
75+
steps:
76+
- uses: actions/checkout@v6
77+
- uses: astral-sh/setup-uv@v8.2.0
78+
with:
79+
enable-cache: true
80+
cache-dependency-glob: "**/pyproject.toml"
81+
- run: uv python install 3.10
82+
- name: Each extra installs and imports in isolation
83+
run: |
84+
set -uo pipefail
85+
# GitHub runs run-steps with `bash -eo pipefail`; disable errexit so a
86+
# venv-create/cleanup hiccup can't abort the loop before every extra is
87+
# checked — failures are collected explicitly in `failed` below.
88+
set +e
89+
extras="orjson sentry pyroscope otl otl-http logging free-all \
90+
fastapi fastapi-sentry fastapi-otl fastapi-logging fastapi-metrics fastapi-all \
91+
litestar litestar-sentry litestar-otl litestar-logging litestar-metrics litestar-all \
92+
faststream faststream-sentry faststream-otl faststream-logging faststream-metrics faststream-all \
93+
fastmcp fastmcp-metrics fastmcp-all"
94+
failed=""
95+
uv venv --python 3.10 .v-bare >/dev/null
96+
if uv pip install --python .v-bare/bin/python . >/dev/null 2>&1 \
97+
&& .v-bare/bin/python -c "import lite_bootstrap"; then :; else failed="$failed __bare__"; fi
98+
rm -rf .v-bare
99+
for e in $extras; do
100+
uv venv --python 3.10 ".v-$e" >/dev/null
101+
if uv pip install --python ".v-$e/bin/python" ".[$e]" >/dev/null 2>&1 \
102+
&& ".v-$e/bin/python" -c "import lite_bootstrap"; then :; else failed="$failed $e"; fi
103+
rm -rf ".v-$e"
104+
done
105+
if [ -n "$failed" ]; then echo "::error::extras failed isolated install+import:$failed"; exit 1; fi
106+
echo "all extras install and import in isolation"

‎lite_bootstrap/bootstrappers/litestar_bootstrapper.py‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,16 @@
3333
from litestar.logging.config import StructLoggingConfig
3434
from litestar.openapi import OpenAPIConfig
3535
from litestar.openapi.plugins import SwaggerRenderPlugin
36-
from litestar.plugins.prometheus import PrometheusConfig, PrometheusController
3736
from litestar.plugins.structlog import StructlogConfig, StructlogPlugin
3837
from litestar.static_files import create_static_files_router
3938

39+
if import_checker.is_litestar_installed and import_checker.is_prometheus_client_installed:
40+
# litestar.plugins.prometheus imports prometheus_client, which the `litestar`
41+
# extra does not install (only `litestar-metrics` does). Used only inside
42+
# LitestarPrometheusInstrument.bootstrap(), gated by check_dependencies() ->
43+
# is_prometheus_client_installed, so this guard matches the usage.
44+
from litestar.plugins.prometheus import PrometheusConfig, PrometheusController
45+
4046
if import_checker.is_litestar_opentelemetry_installed:
4147
from litestar.middleware import ASGIMiddleware
4248
from litestar.types.asgi_types import ASGIApp, Receive, Scope, Send
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
---
2+
summary: Add a CI job that installs each optional extra in isolation and imports lite_bootstrap (catching undeclared transitive dependencies the `--all-extras`-only suite masks), and fix the litestar bug it found — the `litestar.plugins.prometheus` import is guarded by litestar presence but needs prometheus_client, so `lite-bootstrap[litestar]` crashed on import.
3+
---
4+
5+
# Design: Per-extra isolation install-check + fix the litestar/prometheus import
6+
7+
## Summary
8+
9+
Existing CI only ever installs `--all-extras`, so any extra that imports a
10+
package it does not declare passes CI while breaking a real single-extra install.
11+
This shipped twice (the bare-core `typing_extensions` crash in 1.3.1, and the
12+
litestar bug below). Add a CI job that installs **each** extra in its own clean
13+
venv and imports `lite_bootstrap`, and fix the one live bug a local sweep found.
14+
15+
## Motivation
16+
17+
An isolation sweep (clean venv per extra → `import lite_bootstrap`) across all 28
18+
extras on Python 3.10 and 3.12 found:
19+
20+
- **`litestar`, `litestar-sentry`, `litestar-otl`, `litestar-logging` crash on
21+
import** (both versions): `litestar_bootstrapper.py:36` does
22+
`from litestar.plugins.prometheus import PrometheusConfig, PrometheusController`
23+
under the `is_litestar_installed` guard, but that litestar plugin imports
24+
`prometheus_client`, which the `litestar` extra does not install (only
25+
`litestar-metrics` does). `litestar-metrics`/`litestar-all` pass because they
26+
pull `prometheus-client` in. So `pip install lite-bootstrap[litestar]` +
27+
`import lite_bootstrap` raises `MissingDependencyException: prometheus_client`.
28+
- Everything else (bare core + 24 extras) imports clean.
29+
- (`fastmcp` on local macOS-arm 3.10 hit a `cryptography` Rust source-build
30+
failure — a local toolchain artifact, not a bug: `cryptography` supports ≥3.9
31+
via abi3 wheels, so it installs on Linux CI. The check runs on Linux.)
32+
33+
## Design
34+
35+
### 1. Fix the litestar/prometheus import
36+
37+
Move line 36 into its own guard (the usage sites at `litestar_bootstrapper.py:209,219`
38+
already run only when `check_dependencies() → is_prometheus_client_installed`,
39+
so this only tightens the import to match — same shape as the otl-exporter guard):
40+
41+
```python
42+
if import_checker.is_litestar_installed and import_checker.is_prometheus_client_installed:
43+
from litestar.plugins.prometheus import PrometheusConfig, PrometheusController
44+
```
45+
46+
TDD: `emulate_package_missing_with_module_reload("prometheus_client", ["...litestar_bootstrapper"])`
47+
must not crash the reload; assert `import_checker.is_prometheus_client_installed`
48+
is False and the bootstrapper module reimports. Fails on current code, green after.
49+
50+
### 2. Isolation install-check CI job
51+
52+
A single `install-isolation` job in `.github/workflows/_checks.yml`, Python
53+
**3.10** (the floor — the undeclared-dep class is version-independent, and the
54+
floor surfaces the most gated behavior; the `--all-extras` pytest matrix already
55+
covers all versions, and the ft legs cover per-extra installs on 3.13t/3.14t):
56+
57+
```yaml
58+
install-isolation:
59+
runs-on: ubuntu-latest
60+
steps:
61+
- uses: actions/checkout@v6
62+
- uses: astral-sh/setup-uv@v8.2.0
63+
with:
64+
enable-cache: true
65+
cache-dependency-glob: "**/pyproject.toml"
66+
- run: uv python install 3.10
67+
- name: Each extra installs and imports in isolation
68+
run: |
69+
set -uo pipefail
70+
extras="orjson sentry pyroscope otl otl-http logging free-all \
71+
fastapi fastapi-sentry fastapi-otl fastapi-logging fastapi-metrics fastapi-all \
72+
litestar litestar-sentry litestar-otl litestar-logging litestar-metrics litestar-all \
73+
faststream faststream-sentry faststream-otl faststream-logging faststream-metrics faststream-all \
74+
fastmcp fastmcp-metrics fastmcp-all"
75+
failed=""
76+
# bare core first
77+
uv venv --python 3.10 .v-bare >/dev/null
78+
uv pip install --python .v-bare/bin/python . >/dev/null 2>&1 \
79+
&& .v-bare/bin/python -c "import lite_bootstrap" \
80+
|| failed="$failed __bare__"
81+
rm -rf .v-bare
82+
for e in $extras; do
83+
uv venv --python 3.10 ".v-$e" >/dev/null
84+
if uv pip install --python ".v-$e/bin/python" ".[$e]" >/dev/null 2>&1 \
85+
&& ".v-$e/bin/python" -c "import lite_bootstrap"; then :; else failed="$failed $e"; fi
86+
rm -rf ".v-$e"
87+
done
88+
if [ -n "$failed" ]; then echo "::error::extras failed isolated install+import:$failed"; exit 1; fi
89+
echo "all extras install and import in isolation"
90+
```
91+
92+
Collects **all** failures (does not stop at first) and fails listing them.
93+
94+
## Non-goals
95+
96+
- Multi-version isolation matrix (floor-only per the reasoning above).
97+
- Running a bootstrap per extra (import is the cheap, sufficient signal for the
98+
undeclared-dependency class).
99+
100+
## Testing
101+
102+
- The litestar fix's unit test (above); `just test` 100% coverage; `just lint-ci` clean.
103+
- The new CI job is the systemic proof: green means every extra imports in isolation.
104+
105+
## Risk
106+
107+
- **The isolation job surfaces a genuine install gap on 3.10 (e.g. an extra whose
108+
dep truly lacks a 3.10 Linux wheel).** That is the job doing its work; handle
109+
case-by-case (a `python_version` marker on the extra, or documentation). Not
110+
expected for the current matrix.

‎planning/releases/1.3.2.md‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# lite-bootstrap 1.3.2 — fix `lite-bootstrap[litestar]` import
2+
3+
**1.3.2 is a patch release. Fully backward compatible with 1.3.1.**
4+
5+
## Bug fixes
6+
7+
- **`import lite_bootstrap` no longer crashes under `lite-bootstrap[litestar]`
8+
without prometheus-client.** `litestar_bootstrapper` imported
9+
`litestar.plugins.prometheus` (which requires `prometheus_client`) under the
10+
`is_litestar_installed` guard, but the `litestar` extra does not install
11+
prometheus-client — only `litestar-metrics` does. So `pip install
12+
lite-bootstrap[litestar]` followed by `import lite_bootstrap` raised litestar's
13+
`MissingDependencyException: prometheus_client`. The import is now guarded by
14+
prometheus-client presence too (its only uses are inside the metrics
15+
instrument, already gated on that package). Found by a new per-extra isolation
16+
install-check in CI.
17+
18+
## References
19+
20+
- `planning/changes/2026-07-19.04-extra-isolation-install-check.md`

‎tests/test_litestar_bootstrap.py‎

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import dataclasses
22
import gc
3+
import sys
34
import warnings
45
import weakref
56

@@ -16,13 +17,18 @@
1617
from opentelemetry.sdk.trace.export.in_memory_span_exporter import InMemorySpanExporter
1718
from opentelemetry.trace import get_tracer_provider
1819

19-
from lite_bootstrap import LitestarBootstrapper, LitestarConfig
20+
from lite_bootstrap import LitestarBootstrapper, LitestarConfig, import_checker
2021
from lite_bootstrap.bootstrappers.litestar_bootstrapper import (
2122
LitestarOpenTelemetryInstrumentationMiddleware,
2223
build_litestar_route_details_from_scope,
2324
build_span_name,
2425
)
25-
from tests.conftest import CustomInstrumentor, SentryTestTransport, emulate_package_missing
26+
from tests.conftest import (
27+
CustomInstrumentor,
28+
SentryTestTransport,
29+
emulate_package_missing,
30+
emulate_package_missing_with_module_reload,
31+
)
2632

2733

2834
logger = structlog.getLogger(__name__)
@@ -254,3 +260,22 @@ async def transient_app(scope: dict, receive: object, send: object) -> None: #
254260

255261
assert weak_app() is None
256262
assert len(middleware._otel_apps) == 0 # noqa: SLF001
263+
264+
265+
def test_litestar_bootstrap_without_prometheus_client() -> None:
266+
# Regression: `import lite_bootstrap` with the `litestar` extra but not
267+
# prometheus_client crashed because litestar_bootstrapper imported
268+
# `litestar.plugins.prometheus` (which imports prometheus_client) under the
269+
# is_litestar_installed guard alone. Evict the cached litestar.plugins.prometheus
270+
# submodules so the module reload re-runs the real import path (otherwise the
271+
# cached submodule short-circuits it and the bug is masked).
272+
prom_submodules = [name for name in list(sys.modules) if name.startswith("litestar.plugins.prometheus")]
273+
saved = {name: sys.modules.pop(name) for name in prom_submodules}
274+
try:
275+
with emulate_package_missing_with_module_reload(
276+
"prometheus_client",
277+
["lite_bootstrap.bootstrappers.litestar_bootstrapper"],
278+
):
279+
assert import_checker.is_prometheus_client_installed is False
280+
finally:
281+
sys.modules.update(saved)

0 commit comments

Comments
 (0)