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
3 changes: 2 additions & 1 deletion CONTEXT.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@ A *bootstrapper* is ready when its framework package is importable: `is_ready()`
the config, and it is a different question from whether an instrument is **configured**.
_Avoid_: configured, for this sense. The attribute names follow the split: a bootstrapper carries
`not_ready_message`, an instrument carries `not_configured_reason`. An instrument written against
the old `not_ready_message` still works because `BaseInstrument.__init_subclass__` forwards it.
the old `not_ready_message` still works until 2.0: `BaseInstrument.__init_subclass__` forwards it
and emits a `DeprecationWarning`.

**Skipped**:
An instrument the bootstrapper decided not to instantiate. Two paths, deliberately different
Expand Down
13 changes: 11 additions & 2 deletions lite_bootstrap/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import typing

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, FreeBootstrapperConfig, FreeConfig
from lite_bootstrap.bootstrappers.free_bootstrapper import FreeBootstrapper, FreeConfig
from lite_bootstrap.bootstrappers.litestar_bootstrapper import LitestarBootstrapper, LitestarConfig
from lite_bootstrap.exceptions import (
BootstrapperNotReadyError,
Expand All @@ -11,9 +13,14 @@
LiteBootstrapError,
TeardownError,
)
from lite_bootstrap.helpers.warn import deprecated_module_getattr
from lite_bootstrap.instruments.pyroscope_instrument import PyroscopeConfig, PyroscopeInstrument


if typing.TYPE_CHECKING:
from lite_bootstrap.bootstrappers.free_bootstrapper import FreeBootstrapperConfig as FreeBootstrapperConfig


__all__ = [
"BootstrapperNotReadyError",
"ConfigurationError",
Expand All @@ -24,7 +31,6 @@
"FastStreamBootstrapper",
"FastStreamConfig",
"FreeBootstrapper",
"FreeBootstrapperConfig",
"FreeConfig",
"InstrumentDependencyMissingWarning",
"InstrumentSkippedWarning",
Expand All @@ -35,3 +41,6 @@
"PyroscopeInstrument",
"TeardownError",
]


__getattr__ = deprecated_module_getattr(__name__, {"FreeBootstrapperConfig": (FreeConfig, "FreeConfig")})
7 changes: 5 additions & 2 deletions lite_bootstrap/bootstrappers/free_bootstrapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import typing

from lite_bootstrap.bootstrappers.base import BaseBootstrapper
from lite_bootstrap.helpers.warn import deprecated_module_getattr
from lite_bootstrap.instruments.logging_instrument import LoggingConfig, LoggingInstrument
from lite_bootstrap.instruments.opentelemetry_instrument import OpenTelemetryConfig, OpenTelemetryInstrument
from lite_bootstrap.instruments.pyroscope_instrument import PyroscopeConfig, PyroscopeInstrument
Expand Down Expand Up @@ -34,5 +35,7 @@ def _prepare_application(self) -> None:
return None


# Backward-compatible alias preserved for users importing the old name.
FreeBootstrapperConfig = FreeConfig
if typing.TYPE_CHECKING:
FreeBootstrapperConfig = FreeConfig

__getattr__ = deprecated_module_getattr(__name__, {"FreeBootstrapperConfig": (FreeConfig, "FreeConfig")})
24 changes: 24 additions & 0 deletions lite_bootstrap/helpers/warn.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,27 @@ def warn_at_caller(message: str, category: type[Warning] = UserWarning) -> None:
stacklevel += 1
frame = frame.f_back
warnings.warn(message, category=category, stacklevel=stacklevel)


def deprecated_module_getattr(
module_name: str,
aliases: typing.Mapping[str, tuple[object, str | None]],
) -> typing.Callable[[str], object]:
"""Build a module `__getattr__` that resolves each old name to its target with a `DeprecationWarning`.

`aliases` maps an old name to its target and the name that replaces it, or None if nothing does.
"""

def __getattr__(name: str) -> object: # noqa: N807
if name not in aliases:
msg = f"module {module_name!r} has no attribute {name!r}"
raise AttributeError(msg)
target, replacement = aliases[name]
advice = f"use {replacement} instead" if replacement else "nothing replaces it"
warn_at_caller(
f"{name} is deprecated and will be removed in lite-bootstrap 2.0; {advice}.",
category=DeprecationWarning,
)
return target

return __getattr__
7 changes: 7 additions & 0 deletions lite_bootstrap/instruments/base.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import dataclasses
import typing

from lite_bootstrap.helpers.warn import warn_at_caller


@dataclasses.dataclass(kw_only=True, slots=True, frozen=True)
class BaseConfig:
Expand Down Expand Up @@ -53,6 +55,11 @@ def __init_subclass__(cls, **kwargs: object) -> None:
super(BaseInstrument, cls).__init_subclass__(**kwargs)
for legacy_name, current_name in _RENAMED_ATTRIBUTES.items():
if legacy_name in cls.__dict__ and current_name not in cls.__dict__:
warn_at_caller(
f"{cls.__name__}.{legacy_name} is deprecated and will be removed in lite-bootstrap 2.0; "
f"use {current_name} instead.",
category=DeprecationWarning,
)
setattr(cls, current_name, cls.__dict__[legacy_name])

def bootstrap(self) -> None: ...
Expand Down
26 changes: 21 additions & 5 deletions lite_bootstrap/instruments/logging_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,26 +6,39 @@
import typing

from lite_bootstrap import import_checker
from lite_bootstrap.helpers.warn import deprecated_module_getattr


if import_checker.is_orjson_installed:
import orjson


ScopeType = typing.MutableMapping[str, typing.Any]
_ScopeType = typing.MutableMapping[str, typing.Any]


class AddressProtocol(typing.Protocol):
class _AddressProtocol(typing.Protocol):
host: str
port: int


class RequestProtocol(typing.Protocol):
client: AddressProtocol
scope: ScopeType
class _RequestProtocol(typing.Protocol):
client: _AddressProtocol
scope: _ScopeType
method: str


if typing.TYPE_CHECKING:
ScopeType = _ScopeType
AddressProtocol = _AddressProtocol
RequestProtocol = _RequestProtocol

_DEPRECATED_PROTOCOLS: typing.Final = {
"AddressProtocol": (_AddressProtocol, None),
"RequestProtocol": (_RequestProtocol, None),
"ScopeType": (_ScopeType, None),
}


@dataclasses.dataclass(kw_only=True, slots=True, frozen=True)
class _MemoryLoggerFactoryConfig:
logging_buffer_capacity: int
Expand Down Expand Up @@ -141,3 +154,6 @@ def close_handlers(self) -> None:
if target is not None:
target.close()
self._created_handlers.clear()


__getattr__ = deprecated_module_getattr(__name__, _DEPRECATED_PROTOCOLS)
19 changes: 13 additions & 6 deletions lite_bootstrap/instruments/logging_instrument.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,14 @@

from lite_bootstrap import import_checker
from lite_bootstrap.exceptions import collect_teardown_errors
from lite_bootstrap.helpers.warn import deprecated_module_getattr
from lite_bootstrap.instruments.base import BaseConfig, BaseInstrument
from lite_bootstrap.instruments.logging_factory import (
AddressProtocol,
RequestProtocol,
ScopeType,
_DEPRECATED_PROTOCOLS,
_AddressProtocol,
_MemoryLoggerFactoryConfig,
_RequestProtocol,
_ScopeType,
_serialize_log_to_string,
)

Expand Down Expand Up @@ -54,12 +56,9 @@ def tracer_injection(_: "WrappedLogger", __: str, event_dict: "EventDict") -> "E


__all__ = [
"AddressProtocol",
"LoggingConfig",
"LoggingInstrument",
"MemoryLoggerFactory",
"RequestProtocol",
"ScopeType",
"tracer_injection",
]

Expand Down Expand Up @@ -240,3 +239,11 @@ def teardown(self) -> None:
self._logger_factory.close_handlers()
finally:
self._logger_factory = None


if typing.TYPE_CHECKING:
AddressProtocol = _AddressProtocol
RequestProtocol = _RequestProtocol
ScopeType = _ScopeType

__getattr__ = deprecated_module_getattr(__name__, _DEPRECATED_PROTOCOLS)
8 changes: 5 additions & 3 deletions lite_bootstrap/instruments/opentelemetry_instrument.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

from lite_bootstrap import import_checker
from lite_bootstrap.exceptions import InstrumentDependencyMissingWarning, collect_teardown_errors
from lite_bootstrap.helpers.warn import warn_at_caller
from lite_bootstrap.helpers.warn import deprecated_module_getattr, warn_at_caller
from lite_bootstrap.instruments.base import BaseConfig, BaseInstrument


Expand Down Expand Up @@ -372,5 +372,7 @@ def teardown(self) -> None:
self._meter_provider = None


# Backward-compatible alias preserved for users importing the old (lowercase t) spelling.
OpentelemetryConfig = OpenTelemetryConfig
if typing.TYPE_CHECKING:
OpentelemetryConfig = OpenTelemetryConfig

__getattr__ = deprecated_module_getattr(__name__, {"OpentelemetryConfig": (OpenTelemetryConfig, "OpenTelemetryConfig")})
16 changes: 10 additions & 6 deletions lite_bootstrap/instruments/sentry_instrument.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import typing

from lite_bootstrap import import_checker
from lite_bootstrap.helpers.warn import warn_at_caller
from lite_bootstrap.helpers.warn import deprecated_module_getattr, warn_at_caller
from lite_bootstrap.instruments.base import BaseConfig, BaseInstrument
from lite_bootstrap.instruments.logging_factory import STRUCTLOG_META_KEYS, StructuredLogPayload

Expand All @@ -25,11 +25,6 @@
)


# Back-compat alias: this vocabulary moved to logging_factory and was renamed
# STRUCTLOG_META_KEYS. Preserved here for external importers of the old name.
IGNORED_STRUCTLOG_ATTRIBUTES: typing.Final = STRUCTLOG_META_KEYS


@dataclasses.dataclass(kw_only=True, frozen=True)
class SentryConfig(BaseConfig):
sentry_dsn: str | None = None
Expand Down Expand Up @@ -158,3 +153,12 @@ def teardown(self) -> None:
sentry_sdk.flush(timeout=2)
finally:
sentry_sdk.init()


if typing.TYPE_CHECKING:
IGNORED_STRUCTLOG_ATTRIBUTES: typing.Final = STRUCTLOG_META_KEYS

__getattr__ = deprecated_module_getattr(
__name__,
{"IGNORED_STRUCTLOG_ATTRIBUTES": (STRUCTLOG_META_KEYS, "STRUCTLOG_META_KEYS")},
)
11 changes: 8 additions & 3 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,11 @@ def emulate_package_missing(package_name: str) -> Generator[None]:
reload(import_checker)


def _is_module_state(key: str) -> bool:
# A module __getattr__ holds the module's own objects, so it is restored along with them.
return not key.startswith("__") or key == "__getattr__"


@contextlib.contextmanager
def emulate_package_missing_with_module_reload(
package_name: str, module_names: typing.Iterable[str]
Expand All @@ -156,7 +161,7 @@ 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("__")]:
for key in [k for k in mod_dict if _is_module_state(k)]:
del mod_dict[key]
reload(sys.modules[name])

Expand All @@ -168,6 +173,6 @@ def _wipe_and_reload() -> None:
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("__")]:
for key in [k for k in mod_dict if _is_module_state(k)]:
del mod_dict[key]
mod_dict.update({k: v for k, v in snap.items() if not k.startswith("__")})
mod_dict.update({k: v for k, v in snap.items() if _is_module_state(k)})
26 changes: 20 additions & 6 deletions tests/instruments/test_base_instrument.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import dataclasses

import pytest

from lite_bootstrap.instruments.base import BaseConfig, BaseInstrument


Expand All @@ -20,17 +22,29 @@ def test_an_instrument_written_against_the_old_attribute_names_still_works() ->
`dependencies_installed`, which answers True, and construct an instrument whose optional package
is absent.
"""
with pytest.warns(DeprecationWarning, match="is deprecated") as record:

@dataclasses.dataclass(kw_only=True, slots=True)
class LegacyInstrument(BaseInstrument[BaseConfig]):
not_ready_message = "legacy reason"
@dataclasses.dataclass(kw_only=True, slots=True)
class LegacyInstrument(BaseInstrument[BaseConfig]):
not_ready_message = "legacy reason"

@staticmethod
def check_dependencies() -> bool:
return False
@staticmethod
def check_dependencies() -> bool:
return False

assert LegacyInstrument.not_configured_reason == "legacy reason"
assert LegacyInstrument.dependencies_installed() is False
assert [str(warning.message) for warning in record] == [
(
"LegacyInstrument.not_ready_message is deprecated and will be removed in lite-bootstrap 2.0; "
"use not_configured_reason instead."
),
(
"LegacyInstrument.check_dependencies is deprecated and will be removed in lite-bootstrap 2.0; "
"use dependencies_installed instead."
),
]
assert {warning.filename for warning in record} == {__file__}


def test_a_subclass_declaring_both_names_keeps_the_current_one() -> None:
Expand Down
Loading
Loading