From 059ab161b92c2af4c45a9e6a3884e1d0d5bd9882 Mon Sep 17 00:00:00 2001 From: Artur Shiriev Date: Tue, 2 Jun 2026 00:31:55 +0300 Subject: [PATCH] fix: make BaseBootstrapper.bootstrap() idempotent teardown() already short-circuits when is_bootstrapped is False, but bootstrap() previously didn't guard the symmetric case: calling bootstrapper.bootstrap() twice would re-invoke every instrument's bootstrap, causing user-visible side effects (e.g. mounting the FastMCP access-log middleware twice would double every access record; re-registering a Prometheus /metrics route raises). Add the symmetric guard at the top of bootstrap(): when is_bootstrapped, return the prepared application without re-running the instruments. Adds test_bootstrap_is_idempotent next to the existing test_teardown_is_idempotent in test_free_bootstrap.py. Co-Authored-By: Claude Opus 4.7 (1M context) --- lite_bootstrap/bootstrappers/base.py | 2 ++ tests/test_free_bootstrap.py | 15 +++++++++++++++ 2 files changed, 17 insertions(+) diff --git a/lite_bootstrap/bootstrappers/base.py b/lite_bootstrap/bootstrappers/base.py index 938c251..c690e25 100644 --- a/lite_bootstrap/bootstrappers/base.py +++ b/lite_bootstrap/bootstrappers/base.py @@ -74,6 +74,8 @@ def _prepare_application(self) -> ApplicationT: ... def is_ready(self) -> bool: ... def bootstrap(self) -> ApplicationT: + if self.is_bootstrapped: + return self._prepare_application() self.is_bootstrapped = True for one_instrument in self.instruments: one_instrument.bootstrap() diff --git a/tests/test_free_bootstrap.py b/tests/test_free_bootstrap.py index c097a62..0157c77 100644 --- a/tests/test_free_bootstrap.py +++ b/tests/test_free_bootstrap.py @@ -124,3 +124,18 @@ def test_teardown_is_idempotent(free_bootstrapper_config: FreeConfig) -> None: first.teardown.assert_called_once() second.teardown.assert_called_once() assert not bootstrapper.is_bootstrapped + + +def test_bootstrap_is_idempotent(free_bootstrapper_config: FreeConfig) -> None: + bootstrapper = FreeBootstrapper(bootstrap_config=free_bootstrapper_config) + + first = MagicMock() + second = MagicMock() + bootstrapper.instruments = [first, second] + + bootstrapper.bootstrap() + bootstrapper.bootstrap() + + first.bootstrap.assert_called_once() + second.bootstrap.assert_called_once() + assert bootstrapper.is_bootstrapped