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
10 changes: 7 additions & 3 deletions lite_bootstrap/bootstrappers/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,14 +42,18 @@ def __init__(self, bootstrap_config: BaseConfig) -> None:
self.instruments.append(instrument)

def _register_or_skip(self, instrument_type: type[BaseInstrument]) -> BaseInstrument | None:
instrument = instrument_type(bootstrap_config=self.bootstrap_config)
if not instrument.check_dependencies():
# Check dependencies before instantiation: an instrument's __init__
# may reference symbols gated behind an optional import (e.g. a
# default_factory that calls into the missing package), which would
# raise NameError before the check_dependencies skip could run.
if not instrument_type.check_dependencies():
warnings.warn(
instrument.missing_dependency_message,
instrument_type.missing_dependency_message,
category=InstrumentDependencyMissingWarning,
stacklevel=4,
)
return None
instrument = instrument_type(bootstrap_config=self.bootstrap_config)
if not instrument.is_ready():
warnings.warn(
f"{instrument_type.__name__} is not ready: {instrument.not_ready_message}",
Expand Down
5 changes: 4 additions & 1 deletion lite_bootstrap/bootstrappers/faststream_bootstrapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,10 @@ async def check_health(_: object) -> "AsgiResponse":
else AsgiResponse(b"Service is unhealthy", 500, headers={"content-type": "text/plain"})
)

if self.bootstrap_config.opentelemetry_generate_health_check_spans:
if (
self.bootstrap_config.opentelemetry_generate_health_check_spans
and import_checker.is_opentelemetry_installed
):
check_health = tracer.start_as_current_span(f"GET {self.bootstrap_config.health_checks_path}")(
check_health,
)
Expand Down
36 changes: 36 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,3 +58,39 @@ def emulate_package_missing(package_name: str) -> typing.Iterator[None]:
finally:
sys.modules[package_name] = old_module
reload(import_checker)


@contextlib.contextmanager
def emulate_package_missing_with_module_reload(
package_name: str, module_names: typing.Iterable[str]
) -> typing.Iterator[None]:
# Reload listed modules under emulate_package_missing so their
# `if import_checker.is_X_installed: import X` blocks re-evaluate against
# the patched flag. `importlib.reload` preserves existing module globals,
# so we wipe non-dunder names first to truly simulate a fresh import where
# the conditional import never ran.
module_names = list(module_names)
snapshots: dict[str, dict[str, typing.Any]] = {}
for name in module_names:
if name in sys.modules:
snapshots[name] = dict(sys.modules[name].__dict__)

def _wipe_and_reload() -> None:
for name in module_names:
if name in sys.modules:
mod_dict = sys.modules[name].__dict__
for key in [k for k in mod_dict if not k.startswith("__")]:
del mod_dict[key]
reload(sys.modules[name])

with emulate_package_missing(package_name):
_wipe_and_reload()
try:
yield
finally:
for name, snap in snapshots.items():
if name in sys.modules:
mod_dict = sys.modules[name].__dict__
for key in [k for k in mod_dict if not k.startswith("__")]:
del mod_dict[key]
mod_dict.update({k: v for k, v in snap.items() if not k.startswith("__")})
34 changes: 33 additions & 1 deletion tests/test_faststream_bootstrap.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,12 @@
from starlette.testclient import TestClient

from lite_bootstrap import FastStreamBootstrapper, FastStreamConfig
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 @@ -127,3 +132,30 @@ def test_faststream_bootstrapper_with_missing_instrument_dependency(broker: Redi
bootstrap_config = build_faststream_config(broker=broker)
with emulate_package_missing(package_name), pytest.warns(UserWarning, match=package_name):
FastStreamBootstrapper(bootstrap_config=bootstrap_config)


def test_faststream_bootstrap_without_prometheus_client(broker: RedisBroker) -> None:
# Regression: issue #87 bug 1 — FastStreamPrometheusInstrument's
# default_factory called prometheus_client.CollectorRegistry() during
# dataclass __init__, raising NameError before check_dependencies() ran.
bootstrap_config = build_faststream_config(broker=broker)
with emulate_package_missing_with_module_reload(
"prometheus_client",
["lite_bootstrap.bootstrappers.faststream_bootstrapper"],
):
with pytest.warns(UserWarning, match="prometheus_client"):
bootstrapper = FastStreamBootstrapper(bootstrap_config=bootstrap_config)
bootstrapper.bootstrap()


def test_faststream_bootstrap_without_opentelemetry(broker: RedisBroker) -> None:
# Regression: issue #87 bug 2 — FastStreamHealthChecksInstrument.bootstrap
# referenced unbound `tracer` when opentelemetry was absent and
# opentelemetry_generate_health_check_spans defaulted to True.
bootstrap_config = build_faststream_config(broker=broker)
with emulate_package_missing_with_module_reload(
"opentelemetry",
["lite_bootstrap.bootstrappers.faststream_bootstrapper"],
):
bootstrapper = FastStreamBootstrapper(bootstrap_config=bootstrap_config)
bootstrapper.bootstrap()
Loading