Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions .github/workflows/_checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -69,3 +69,38 @@ jobs:
run: uv pip install ".[${{ matrix.extras }}]"
- name: Free-threaded smoke test
run: .venv/bin/python scripts/ft_smoke.py

install-isolation:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: astral-sh/setup-uv@v8.2.0
with:
enable-cache: true
cache-dependency-glob: "**/pyproject.toml"
- run: uv python install 3.10
- name: Each extra installs and imports in isolation
run: |
set -uo pipefail
# GitHub runs run-steps with `bash -eo pipefail`; disable errexit so a
# venv-create/cleanup hiccup can't abort the loop before every extra is
# checked — failures are collected explicitly in `failed` below.
set +e
extras="orjson sentry pyroscope otl otl-http logging free-all \
fastapi fastapi-sentry fastapi-otl fastapi-logging fastapi-metrics fastapi-all \
litestar litestar-sentry litestar-otl litestar-logging litestar-metrics litestar-all \
faststream faststream-sentry faststream-otl faststream-logging faststream-metrics faststream-all \
fastmcp fastmcp-metrics fastmcp-all"
failed=""
uv venv --python 3.10 .v-bare >/dev/null
if uv pip install --python .v-bare/bin/python . >/dev/null 2>&1 \
&& .v-bare/bin/python -c "import lite_bootstrap"; then :; else failed="$failed __bare__"; fi
rm -rf .v-bare
for e in $extras; do
uv venv --python 3.10 ".v-$e" >/dev/null
if uv pip install --python ".v-$e/bin/python" ".[$e]" >/dev/null 2>&1 \
&& ".v-$e/bin/python" -c "import lite_bootstrap"; then :; else failed="$failed $e"; fi
rm -rf ".v-$e"
done
if [ -n "$failed" ]; then echo "::error::extras failed isolated install+import:$failed"; exit 1; fi
echo "all extras install and import in isolation"
8 changes: 7 additions & 1 deletion lite_bootstrap/bootstrappers/litestar_bootstrapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,16 @@
from litestar.logging.config import StructLoggingConfig
from litestar.openapi import OpenAPIConfig
from litestar.openapi.plugins import SwaggerRenderPlugin
from litestar.plugins.prometheus import PrometheusConfig, PrometheusController
from litestar.plugins.structlog import StructlogConfig, StructlogPlugin
from litestar.static_files import create_static_files_router

if import_checker.is_litestar_installed and import_checker.is_prometheus_client_installed:
# litestar.plugins.prometheus imports prometheus_client, which the `litestar`
# extra does not install (only `litestar-metrics` does). Used only inside
# LitestarPrometheusInstrument.bootstrap(), gated by check_dependencies() ->
# is_prometheus_client_installed, so this guard matches the usage.
from litestar.plugins.prometheus import PrometheusConfig, PrometheusController

if import_checker.is_litestar_opentelemetry_installed:
from litestar.middleware import ASGIMiddleware
from litestar.types.asgi_types import ASGIApp, Receive, Scope, Send
Expand Down
110 changes: 110 additions & 0 deletions planning/changes/2026-07-19.04-extra-isolation-install-check.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
---
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.
---

# Design: Per-extra isolation install-check + fix the litestar/prometheus import

## Summary

Existing CI only ever installs `--all-extras`, so any extra that imports a
package it does not declare passes CI while breaking a real single-extra install.
This shipped twice (the bare-core `typing_extensions` crash in 1.3.1, and the
litestar bug below). Add a CI job that installs **each** extra in its own clean
venv and imports `lite_bootstrap`, and fix the one live bug a local sweep found.

## Motivation

An isolation sweep (clean venv per extra → `import lite_bootstrap`) across all 28
extras on Python 3.10 and 3.12 found:

- **`litestar`, `litestar-sentry`, `litestar-otl`, `litestar-logging` crash on
import** (both versions): `litestar_bootstrapper.py:36` does
`from litestar.plugins.prometheus import PrometheusConfig, PrometheusController`
under the `is_litestar_installed` guard, but that litestar plugin imports
`prometheus_client`, which the `litestar` extra does not install (only
`litestar-metrics` does). `litestar-metrics`/`litestar-all` pass because they
pull `prometheus-client` in. So `pip install lite-bootstrap[litestar]` +
`import lite_bootstrap` raises `MissingDependencyException: prometheus_client`.
- Everything else (bare core + 24 extras) imports clean.
- (`fastmcp` on local macOS-arm 3.10 hit a `cryptography` Rust source-build
failure — a local toolchain artifact, not a bug: `cryptography` supports ≥3.9
via abi3 wheels, so it installs on Linux CI. The check runs on Linux.)

## Design

### 1. Fix the litestar/prometheus import

Move line 36 into its own guard (the usage sites at `litestar_bootstrapper.py:209,219`
already run only when `check_dependencies() → is_prometheus_client_installed`,
so this only tightens the import to match — same shape as the otl-exporter guard):

```python
if import_checker.is_litestar_installed and import_checker.is_prometheus_client_installed:
from litestar.plugins.prometheus import PrometheusConfig, PrometheusController
```

TDD: `emulate_package_missing_with_module_reload("prometheus_client", ["...litestar_bootstrapper"])`
must not crash the reload; assert `import_checker.is_prometheus_client_installed`
is False and the bootstrapper module reimports. Fails on current code, green after.

### 2. Isolation install-check CI job

A single `install-isolation` job in `.github/workflows/_checks.yml`, Python
**3.10** (the floor — the undeclared-dep class is version-independent, and the
floor surfaces the most gated behavior; the `--all-extras` pytest matrix already
covers all versions, and the ft legs cover per-extra installs on 3.13t/3.14t):

```yaml
install-isolation:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: astral-sh/setup-uv@v8.2.0
with:
enable-cache: true
cache-dependency-glob: "**/pyproject.toml"
- run: uv python install 3.10
- name: Each extra installs and imports in isolation
run: |
set -uo pipefail
extras="orjson sentry pyroscope otl otl-http logging free-all \
fastapi fastapi-sentry fastapi-otl fastapi-logging fastapi-metrics fastapi-all \
litestar litestar-sentry litestar-otl litestar-logging litestar-metrics litestar-all \
faststream faststream-sentry faststream-otl faststream-logging faststream-metrics faststream-all \
fastmcp fastmcp-metrics fastmcp-all"
failed=""
# bare core first
uv venv --python 3.10 .v-bare >/dev/null
uv pip install --python .v-bare/bin/python . >/dev/null 2>&1 \
&& .v-bare/bin/python -c "import lite_bootstrap" \
|| failed="$failed __bare__"
rm -rf .v-bare
for e in $extras; do
uv venv --python 3.10 ".v-$e" >/dev/null
if uv pip install --python ".v-$e/bin/python" ".[$e]" >/dev/null 2>&1 \
&& ".v-$e/bin/python" -c "import lite_bootstrap"; then :; else failed="$failed $e"; fi
rm -rf ".v-$e"
done
if [ -n "$failed" ]; then echo "::error::extras failed isolated install+import:$failed"; exit 1; fi
echo "all extras install and import in isolation"
```

Collects **all** failures (does not stop at first) and fails listing them.

## Non-goals

- Multi-version isolation matrix (floor-only per the reasoning above).
- Running a bootstrap per extra (import is the cheap, sufficient signal for the
undeclared-dependency class).

## Testing

- The litestar fix's unit test (above); `just test` 100% coverage; `just lint-ci` clean.
- The new CI job is the systemic proof: green means every extra imports in isolation.

## Risk

- **The isolation job surfaces a genuine install gap on 3.10 (e.g. an extra whose
dep truly lacks a 3.10 Linux wheel).** That is the job doing its work; handle
case-by-case (a `python_version` marker on the extra, or documentation). Not
expected for the current matrix.
20 changes: 20 additions & 0 deletions planning/releases/1.3.2.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# lite-bootstrap 1.3.2 — fix `lite-bootstrap[litestar]` import

**1.3.2 is a patch release. Fully backward compatible with 1.3.1.**

## Bug fixes

- **`import lite_bootstrap` no longer crashes under `lite-bootstrap[litestar]`
without prometheus-client.** `litestar_bootstrapper` imported
`litestar.plugins.prometheus` (which requires `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]` followed by `import lite_bootstrap` raised litestar's
`MissingDependencyException: prometheus_client`. The import is now guarded by
prometheus-client presence too (its only uses are inside the metrics
instrument, already gated on that package). Found by a new per-extra isolation
install-check in CI.

## References

- `planning/changes/2026-07-19.04-extra-isolation-install-check.md`
29 changes: 27 additions & 2 deletions tests/test_litestar_bootstrap.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import dataclasses
import gc
import sys
import warnings
import weakref

Expand All @@ -16,13 +17,18 @@
from opentelemetry.sdk.trace.export.in_memory_span_exporter import InMemorySpanExporter
from opentelemetry.trace import get_tracer_provider

from lite_bootstrap import LitestarBootstrapper, LitestarConfig
from lite_bootstrap import LitestarBootstrapper, LitestarConfig, import_checker
from lite_bootstrap.bootstrappers.litestar_bootstrapper import (
LitestarOpenTelemetryInstrumentationMiddleware,
build_litestar_route_details_from_scope,
build_span_name,
)
from tests.conftest import CustomInstrumentor, SentryTestTransport, emulate_package_missing
from tests.conftest import (
CustomInstrumentor,
SentryTestTransport,
emulate_package_missing,
emulate_package_missing_with_module_reload,
)


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

assert weak_app() is None
assert len(middleware._otel_apps) == 0 # noqa: SLF001


def test_litestar_bootstrap_without_prometheus_client() -> None:
# Regression: `import lite_bootstrap` with the `litestar` extra but not
# prometheus_client crashed because litestar_bootstrapper imported
# `litestar.plugins.prometheus` (which imports prometheus_client) under the
# is_litestar_installed guard alone. Evict the cached litestar.plugins.prometheus
# submodules so the module reload re-runs the real import path (otherwise the
# cached submodule short-circuits it and the bug is masked).
prom_submodules = [name for name in list(sys.modules) if name.startswith("litestar.plugins.prometheus")]
saved = {name: sys.modules.pop(name) for name in prom_submodules}
try:
with emulate_package_missing_with_module_reload(
"prometheus_client",
["lite_bootstrap.bootstrappers.litestar_bootstrapper"],
):
assert import_checker.is_prometheus_client_installed is False
finally:
sys.modules.update(saved)