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
48 changes: 42 additions & 6 deletions lite_bootstrap/__init__.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,22 @@
from lite_bootstrap.bootstrappers.fastapi_bootstrapper import FastAPIBootstrapper, FastAPIConfig
from lite_bootstrap.bootstrappers.fastmcp_bootstrapper import FastMcpBootstrapper, FastMcpConfig
from lite_bootstrap.bootstrappers.faststream_bootstrapper import FastStreamBootstrapper, FastStreamConfig
from lite_bootstrap.bootstrappers.free_bootstrapper import FreeBootstrapper, FreeConfig
from lite_bootstrap.bootstrappers.litestar_bootstrapper import LitestarBootstrapper, LitestarConfig
import typing
from importlib import import_module

from lite_bootstrap.exceptions import (
BootstrapperNotReadyError,
ConfigurationError,
InstrumentDependencyMissingWarning,
LiteBootstrapError,
TeardownError,
)
from lite_bootstrap.instruments.pyroscope_instrument import PyroscopeConfig, PyroscopeInstrument


if typing.TYPE_CHECKING:
from lite_bootstrap.bootstrappers.fastapi_bootstrapper import FastAPIBootstrapper, FastAPIConfig
from lite_bootstrap.bootstrappers.fastmcp_bootstrapper import FastMcpBootstrapper, FastMcpConfig
from lite_bootstrap.bootstrappers.faststream_bootstrapper import FastStreamBootstrapper, FastStreamConfig
from lite_bootstrap.bootstrappers.free_bootstrapper import FreeBootstrapper, FreeConfig
from lite_bootstrap.bootstrappers.litestar_bootstrapper import LitestarBootstrapper, LitestarConfig
from lite_bootstrap.instruments.pyroscope_instrument import PyroscopeConfig, PyroscopeInstrument


__all__ = [
Expand All @@ -32,3 +38,33 @@
"PyroscopeInstrument",
"TeardownError",
]


_LAZY_EXPORTS = {
"FastAPIBootstrapper": "bootstrappers.fastapi_bootstrapper",
"FastAPIConfig": "bootstrappers.fastapi_bootstrapper",
"FastMcpBootstrapper": "bootstrappers.fastmcp_bootstrapper",
"FastMcpConfig": "bootstrappers.fastmcp_bootstrapper",
"FastStreamBootstrapper": "bootstrappers.faststream_bootstrapper",
"FastStreamConfig": "bootstrappers.faststream_bootstrapper",
"FreeBootstrapper": "bootstrappers.free_bootstrapper",
"FreeConfig": "bootstrappers.free_bootstrapper",
"LitestarBootstrapper": "bootstrappers.litestar_bootstrapper",
"LitestarConfig": "bootstrappers.litestar_bootstrapper",
"PyroscopeConfig": "instruments.pyroscope_instrument",
"PyroscopeInstrument": "instruments.pyroscope_instrument",
}


def __getattr__(name: str) -> typing.Any: # noqa: ANN401
module_name = _LAZY_EXPORTS.get(name)
if module_name is None:
msg = f"module {__name__!r} has no attribute {name!r}"
raise AttributeError(msg)
exported = getattr(import_module(f".{module_name}", __name__), name)
globals()[name] = exported
return exported


def __dir__() -> list[str]:
return sorted(set(globals()) | set(__all__))
87 changes: 87 additions & 0 deletions tests/test_core_import_surface.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import subprocess
import sys

import pytest
import typing_extensions

import lite_bootstrap
Expand Down Expand Up @@ -95,3 +96,89 @@ def test_import_with_bare_opentelemetry_namespace(tmp_path: pathlib.Path) -> Non
)

assert result.returncode == 0, result.stderr


@pytest.mark.parametrize(
"statement",
[
"import lite_bootstrap",
"from lite_bootstrap import ConfigurationError",
"from lite_bootstrap import PyroscopeConfig",
"from lite_bootstrap import FastAPIBootstrapper",
],
)
def test_public_imports_do_not_load_unrelated_frameworks(statement: str) -> None:
result = subprocess.run( # noqa: S603
[
sys.executable,
"-c",
statement
+ """
import sys
assert not {"litestar", "faststream", "fastmcp"}.intersection(sys.modules)
""",
],
capture_output=True,
text=True,
check=False,
)
assert result.returncode == 0, result.stderr


def test_every_public_export_resolves_in_a_fresh_process() -> None:
result = subprocess.run(
[
sys.executable,
"-c",
"""
import lite_bootstrap

for name in lite_bootstrap.__all__:
exported = getattr(lite_bootstrap, name)
assert exported.__name__ == name, name
assert exported is getattr(lite_bootstrap, name), name

try:
getattr(lite_bootstrap, "not_an_export")
except AttributeError as error:
assert "not_an_export" in str(error)
else:
raise AssertionError("unknown export resolved")
""",
],
capture_output=True,
text=True,
check=False,
)
assert result.returncode == 0, result.stderr


def test_unknown_public_export_raises_attribute_error() -> None:
name = "not_an_export"
with pytest.raises(AttributeError, match=name):
getattr(lite_bootstrap, name)


def test_public_exports_are_discoverable_without_loading_frameworks() -> None:
result = subprocess.run(
[
sys.executable,
"-c",
"""
import sys
import lite_bootstrap

before = set(sys.modules)
names = dir(lite_bootstrap)
assert set(lite_bootstrap.__all__).issubset(names)
assert "__name__" in names
assert set(sys.modules) == before
assert not {"fastapi", "litestar", "faststream", "fastmcp"}.intersection(sys.modules)
""",
],
capture_output=True,
text=True,
check=False,
)
assert result.returncode == 0, result.stderr
assert set(lite_bootstrap.__all__).issubset(dir(lite_bootstrap))
Loading