From d920008c0a36a83b539269b044c6a47d3b9ccb3f Mon Sep 17 00:00:00 2001 From: Artur Shiriev Date: Sun, 19 Jul 2026 17:36:15 +0300 Subject: [PATCH 1/3] 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) --- ...-07-19.04-extra-isolation-install-check.md | 110 ++++++++++++++++++ 1 file changed, 110 insertions(+) create mode 100644 planning/changes/2026-07-19.04-extra-isolation-install-check.md diff --git a/planning/changes/2026-07-19.04-extra-isolation-install-check.md b/planning/changes/2026-07-19.04-extra-isolation-install-check.md new file mode 100644 index 0000000..1413172 --- /dev/null +++ b/planning/changes/2026-07-19.04-extra-isolation-install-check.md @@ -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. From 3cfe041ae00e1848d0686ae28af3d0f47bbeb5af Mon Sep 17 00:00:00 2001 From: Artur Shiriev Date: Sun, 19 Jul 2026 17:48:05 +0300 Subject: [PATCH 2/3] fix(litestar): guard the prometheus plugin import behind prometheus_client MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- .../bootstrappers/litestar_bootstrapper.py | 8 ++++- planning/releases/1.3.2.md | 20 +++++++++++++ tests/test_litestar_bootstrap.py | 29 +++++++++++++++++-- 3 files changed, 54 insertions(+), 3 deletions(-) create mode 100644 planning/releases/1.3.2.md diff --git a/lite_bootstrap/bootstrappers/litestar_bootstrapper.py b/lite_bootstrap/bootstrappers/litestar_bootstrapper.py index fe265ae..6509e77 100644 --- a/lite_bootstrap/bootstrappers/litestar_bootstrapper.py +++ b/lite_bootstrap/bootstrappers/litestar_bootstrapper.py @@ -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 diff --git a/planning/releases/1.3.2.md b/planning/releases/1.3.2.md new file mode 100644 index 0000000..b68067f --- /dev/null +++ b/planning/releases/1.3.2.md @@ -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` diff --git a/tests/test_litestar_bootstrap.py b/tests/test_litestar_bootstrap.py index b81cd1d..3d00f72 100644 --- a/tests/test_litestar_bootstrap.py +++ b/tests/test_litestar_bootstrap.py @@ -1,5 +1,6 @@ import dataclasses import gc +import sys import warnings import weakref @@ -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__) @@ -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) From 5ba8b23a52833d435150dacc7e13afd5807f4fdb Mon Sep 17 00:00:00 2001 From: Artur Shiriev Date: Sun, 19 Jul 2026 17:56:48 +0300 Subject: [PATCH 3/3] 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) --- .github/workflows/_checks.yml | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/.github/workflows/_checks.yml b/.github/workflows/_checks.yml index 985efe1..533b489 100644 --- a/.github/workflows/_checks.yml +++ b/.github/workflows/_checks.yml @@ -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"