Skip to content

Commit d80ebc2

Browse files
authored
feat: warn on deprecated aliases ahead of their removal in 2.0 (#350)
1 parent 9b045e6 commit d80ebc2

12 files changed

Lines changed: 188 additions & 34 deletions

File tree

‎CONTEXT.md‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,8 @@ A *bootstrapper* is ready when its framework package is importable: `is_ready()`
3838
the config, and it is a different question from whether an instrument is **configured**.
3939
_Avoid_: configured, for this sense. The attribute names follow the split: a bootstrapper carries
4040
`not_ready_message`, an instrument carries `not_configured_reason`. An instrument written against
41-
the old `not_ready_message` still works because `BaseInstrument.__init_subclass__` forwards it.
41+
the old `not_ready_message` still works until 2.0: `BaseInstrument.__init_subclass__` forwards it
42+
and emits a `DeprecationWarning`.
4243

4344
**Skipped**:
4445
An instrument the bootstrapper decided not to instantiate. Two paths, deliberately different

‎lite_bootstrap/__init__.py‎

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1+
import typing
2+
13
from lite_bootstrap.bootstrappers.fastapi_bootstrapper import FastAPIBootstrapper, FastAPIConfig
24
from lite_bootstrap.bootstrappers.fastmcp_bootstrapper import FastMcpBootstrapper, FastMcpConfig
35
from lite_bootstrap.bootstrappers.faststream_bootstrapper import FastStreamBootstrapper, FastStreamConfig
4-
from lite_bootstrap.bootstrappers.free_bootstrapper import FreeBootstrapper, FreeBootstrapperConfig, FreeConfig
6+
from lite_bootstrap.bootstrappers.free_bootstrapper import FreeBootstrapper, FreeConfig
57
from lite_bootstrap.bootstrappers.litestar_bootstrapper import LitestarBootstrapper, LitestarConfig
68
from lite_bootstrap.exceptions import (
79
BootstrapperNotReadyError,
@@ -11,9 +13,14 @@
1113
LiteBootstrapError,
1214
TeardownError,
1315
)
16+
from lite_bootstrap.helpers.warn import deprecated_module_getattr
1417
from lite_bootstrap.instruments.pyroscope_instrument import PyroscopeConfig, PyroscopeInstrument
1518

1619

20+
if typing.TYPE_CHECKING:
21+
from lite_bootstrap.bootstrappers.free_bootstrapper import FreeBootstrapperConfig as FreeBootstrapperConfig
22+
23+
1724
__all__ = [
1825
"BootstrapperNotReadyError",
1926
"ConfigurationError",
@@ -24,7 +31,6 @@
2431
"FastStreamBootstrapper",
2532
"FastStreamConfig",
2633
"FreeBootstrapper",
27-
"FreeBootstrapperConfig",
2834
"FreeConfig",
2935
"InstrumentDependencyMissingWarning",
3036
"InstrumentSkippedWarning",
@@ -35,3 +41,6 @@
3541
"PyroscopeInstrument",
3642
"TeardownError",
3743
]
44+
45+
46+
__getattr__ = deprecated_module_getattr(__name__, {"FreeBootstrapperConfig": (FreeConfig, "FreeConfig")})

‎lite_bootstrap/bootstrappers/free_bootstrapper.py‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import typing
33

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

3637

37-
# Backward-compatible alias preserved for users importing the old name.
38-
FreeBootstrapperConfig = FreeConfig
38+
if typing.TYPE_CHECKING:
39+
FreeBootstrapperConfig = FreeConfig
40+
41+
__getattr__ = deprecated_module_getattr(__name__, {"FreeBootstrapperConfig": (FreeConfig, "FreeConfig")})

‎lite_bootstrap/helpers/warn.py‎

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,3 +34,27 @@ def warn_at_caller(message: str, category: type[Warning] = UserWarning) -> None:
3434
stacklevel += 1
3535
frame = frame.f_back
3636
warnings.warn(message, category=category, stacklevel=stacklevel)
37+
38+
39+
def deprecated_module_getattr(
40+
module_name: str,
41+
aliases: typing.Mapping[str, tuple[object, str | None]],
42+
) -> typing.Callable[[str], object]:
43+
"""Build a module `__getattr__` that resolves each old name to its target with a `DeprecationWarning`.
44+
45+
`aliases` maps an old name to its target and the name that replaces it, or None if nothing does.
46+
"""
47+
48+
def __getattr__(name: str) -> object: # noqa: N807
49+
if name not in aliases:
50+
msg = f"module {module_name!r} has no attribute {name!r}"
51+
raise AttributeError(msg)
52+
target, replacement = aliases[name]
53+
advice = f"use {replacement} instead" if replacement else "nothing replaces it"
54+
warn_at_caller(
55+
f"{name} is deprecated and will be removed in lite-bootstrap 2.0; {advice}.",
56+
category=DeprecationWarning,
57+
)
58+
return target
59+
60+
return __getattr__

‎lite_bootstrap/instruments/base.py‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import dataclasses
22
import typing
33

4+
from lite_bootstrap.helpers.warn import warn_at_caller
5+
46

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

5865
def bootstrap(self) -> None: ...

‎lite_bootstrap/instruments/logging_factory.py‎

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,26 +6,39 @@
66
import typing
77

88
from lite_bootstrap import import_checker
9+
from lite_bootstrap.helpers.warn import deprecated_module_getattr
910

1011

1112
if import_checker.is_orjson_installed:
1213
import orjson
1314

1415

15-
ScopeType = typing.MutableMapping[str, typing.Any]
16+
_ScopeType = typing.MutableMapping[str, typing.Any]
1617

1718

18-
class AddressProtocol(typing.Protocol):
19+
class _AddressProtocol(typing.Protocol):
1920
host: str
2021
port: int
2122

2223

23-
class RequestProtocol(typing.Protocol):
24-
client: AddressProtocol
25-
scope: ScopeType
24+
class _RequestProtocol(typing.Protocol):
25+
client: _AddressProtocol
26+
scope: _ScopeType
2627
method: str
2728

2829

30+
if typing.TYPE_CHECKING:
31+
ScopeType = _ScopeType
32+
AddressProtocol = _AddressProtocol
33+
RequestProtocol = _RequestProtocol
34+
35+
_DEPRECATED_PROTOCOLS: typing.Final = {
36+
"AddressProtocol": (_AddressProtocol, None),
37+
"RequestProtocol": (_RequestProtocol, None),
38+
"ScopeType": (_ScopeType, None),
39+
}
40+
41+
2942
@dataclasses.dataclass(kw_only=True, slots=True, frozen=True)
3043
class _MemoryLoggerFactoryConfig:
3144
logging_buffer_capacity: int
@@ -141,3 +154,6 @@ def close_handlers(self) -> None:
141154
if target is not None:
142155
target.close()
143156
self._created_handlers.clear()
157+
158+
159+
__getattr__ = deprecated_module_getattr(__name__, _DEPRECATED_PROTOCOLS)

‎lite_bootstrap/instruments/logging_instrument.py‎

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,14 @@
55

66
from lite_bootstrap import import_checker
77
from lite_bootstrap.exceptions import collect_teardown_errors
8+
from lite_bootstrap.helpers.warn import deprecated_module_getattr
89
from lite_bootstrap.instruments.base import BaseConfig, BaseInstrument
910
from lite_bootstrap.instruments.logging_factory import (
10-
AddressProtocol,
11-
RequestProtocol,
12-
ScopeType,
11+
_DEPRECATED_PROTOCOLS,
12+
_AddressProtocol,
1313
_MemoryLoggerFactoryConfig,
14+
_RequestProtocol,
15+
_ScopeType,
1416
_serialize_log_to_string,
1517
)
1618

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

5557

5658
__all__ = [
57-
"AddressProtocol",
5859
"LoggingConfig",
5960
"LoggingInstrument",
6061
"MemoryLoggerFactory",
61-
"RequestProtocol",
62-
"ScopeType",
6362
"tracer_injection",
6463
]
6564

@@ -240,3 +239,11 @@ def teardown(self) -> None:
240239
self._logger_factory.close_handlers()
241240
finally:
242241
self._logger_factory = None
242+
243+
244+
if typing.TYPE_CHECKING:
245+
AddressProtocol = _AddressProtocol
246+
RequestProtocol = _RequestProtocol
247+
ScopeType = _ScopeType
248+
249+
__getattr__ = deprecated_module_getattr(__name__, _DEPRECATED_PROTOCOLS)

‎lite_bootstrap/instruments/opentelemetry_instrument.py‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
from lite_bootstrap import import_checker
99
from lite_bootstrap.exceptions import InstrumentDependencyMissingWarning, collect_teardown_errors
10-
from lite_bootstrap.helpers.warn import warn_at_caller
10+
from lite_bootstrap.helpers.warn import deprecated_module_getattr, warn_at_caller
1111
from lite_bootstrap.instruments.base import BaseConfig, BaseInstrument
1212

1313

@@ -372,5 +372,7 @@ def teardown(self) -> None:
372372
self._meter_provider = None
373373

374374

375-
# Backward-compatible alias preserved for users importing the old (lowercase t) spelling.
376-
OpentelemetryConfig = OpenTelemetryConfig
375+
if typing.TYPE_CHECKING:
376+
OpentelemetryConfig = OpenTelemetryConfig
377+
378+
__getattr__ = deprecated_module_getattr(__name__, {"OpentelemetryConfig": (OpenTelemetryConfig, "OpenTelemetryConfig")})

‎lite_bootstrap/instruments/sentry_instrument.py‎

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import typing
55

66
from lite_bootstrap import import_checker
7-
from lite_bootstrap.helpers.warn import warn_at_caller
7+
from lite_bootstrap.helpers.warn import deprecated_module_getattr, warn_at_caller
88
from lite_bootstrap.instruments.base import BaseConfig, BaseInstrument
99
from lite_bootstrap.instruments.logging_factory import STRUCTLOG_META_KEYS, StructuredLogPayload
1010

@@ -25,11 +25,6 @@
2525
)
2626

2727

28-
# Back-compat alias: this vocabulary moved to logging_factory and was renamed
29-
# STRUCTLOG_META_KEYS. Preserved here for external importers of the old name.
30-
IGNORED_STRUCTLOG_ATTRIBUTES: typing.Final = STRUCTLOG_META_KEYS
31-
32-
3328
@dataclasses.dataclass(kw_only=True, frozen=True)
3429
class SentryConfig(BaseConfig):
3530
sentry_dsn: str | None = None
@@ -158,3 +153,12 @@ def teardown(self) -> None:
158153
sentry_sdk.flush(timeout=2)
159154
finally:
160155
sentry_sdk.init()
156+
157+
158+
if typing.TYPE_CHECKING:
159+
IGNORED_STRUCTLOG_ATTRIBUTES: typing.Final = STRUCTLOG_META_KEYS
160+
161+
__getattr__ = deprecated_module_getattr(
162+
__name__,
163+
{"IGNORED_STRUCTLOG_ATTRIBUTES": (STRUCTLOG_META_KEYS, "STRUCTLOG_META_KEYS")},
164+
)

‎tests/conftest.py‎

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,11 @@ def emulate_package_missing(package_name: str) -> Generator[None]:
137137
reload(import_checker)
138138

139139

140+
def _is_module_state(key: str) -> bool:
141+
# A module __getattr__ holds the module's own objects, so it is restored along with them.
142+
return not key.startswith("__") or key == "__getattr__"
143+
144+
140145
@contextlib.contextmanager
141146
def emulate_package_missing_with_module_reload(
142147
package_name: str, module_names: typing.Iterable[str]
@@ -156,7 +161,7 @@ def _wipe_and_reload() -> None:
156161
for name in module_names:
157162
if name in sys.modules:
158163
mod_dict = sys.modules[name].__dict__
159-
for key in [k for k in mod_dict if not k.startswith("__")]:
164+
for key in [k for k in mod_dict if _is_module_state(k)]:
160165
del mod_dict[key]
161166
reload(sys.modules[name])
162167

@@ -168,6 +173,6 @@ def _wipe_and_reload() -> None:
168173
for name, snap in snapshots.items():
169174
if name in sys.modules:
170175
mod_dict = sys.modules[name].__dict__
171-
for key in [k for k in mod_dict if not k.startswith("__")]:
176+
for key in [k for k in mod_dict if _is_module_state(k)]:
172177
del mod_dict[key]
173-
mod_dict.update({k: v for k, v in snap.items() if not k.startswith("__")})
178+
mod_dict.update({k: v for k, v in snap.items() if _is_module_state(k)})

0 commit comments

Comments
 (0)