From 6b7507abc0a0e92ac29f784f4ada971f35345287 Mon Sep 17 00:00:00 2001 From: Artur Shiriev Date: Thu, 29 Oct 2026 16:01:13 +0300 Subject: [PATCH] Remove the names and config fields deprecated in 1.x --- CONTEXT.md | 4 +- lite_bootstrap/__init__.py | 10 -- .../bootstrappers/fastapi_bootstrapper.py | 17 ---- .../bootstrappers/fastmcp_bootstrapper.py | 14 --- .../bootstrappers/free_bootstrapper.py | 7 -- .../bootstrappers/litestar_bootstrapper.py | 10 -- lite_bootstrap/helpers/warn.py | 3 +- lite_bootstrap/instruments/base.py | 24 +---- lite_bootstrap/instruments/logging_factory.py | 30 ------ .../instruments/logging_instrument.py | 13 --- .../instruments/opentelemetry_instrument.py | 8 +- .../instruments/sentry_instrument.py | 13 +-- tests/instruments/test_base_instrument.py | 48 ---------- tests/test_deprecated_aliases.py | 70 ++++++-------- tests/test_renamed_config_fields.py | 94 +++++++++---------- 15 files changed, 82 insertions(+), 283 deletions(-) diff --git a/CONTEXT.md b/CONTEXT.md index 24e5fa8..5f22f6e 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -37,9 +37,7 @@ A *bootstrapper* is ready when its framework package is importable: `is_ready()` `__init__`, raising `BootstrapperNotReadyError` when false. It is about the environment, never about 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 until 2.0: `BaseInstrument.__init_subclass__` forwards it -and emits a `DeprecationWarning`. +`not_ready_message`, an instrument carries `not_configured_reason`. **Skipped**: An instrument the bootstrapper decided not to instantiate. Two paths, deliberately different diff --git a/lite_bootstrap/__init__.py b/lite_bootstrap/__init__.py index 5f69aee..24a036a 100644 --- a/lite_bootstrap/__init__.py +++ b/lite_bootstrap/__init__.py @@ -1,5 +1,3 @@ -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 @@ -13,14 +11,9 @@ 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", @@ -41,6 +34,3 @@ "PyroscopeInstrument", "TeardownError", ] - - -__getattr__ = deprecated_module_getattr(__name__, {"FreeBootstrapperConfig": (FreeConfig, "FreeConfig")}) diff --git a/lite_bootstrap/bootstrappers/fastapi_bootstrapper.py b/lite_bootstrap/bootstrappers/fastapi_bootstrapper.py index f50d1e0..338e8d9 100644 --- a/lite_bootstrap/bootstrappers/fastapi_bootstrapper.py +++ b/lite_bootstrap/bootstrappers/fastapi_bootstrapper.py @@ -80,23 +80,6 @@ class FastAPIConfig( "starlette": "StarletteIntegration", "fastapi": "FastApiIntegration", } - # Deprecated names, removed in 2.0. - _renamed_fields: typing.ClassVar[Mapping[str, str]] = { - "prometheus_instrumentator_params": "prometheus_instrumentator_additional_params", - "prometheus_instrument_params": "prometheus_instrument_additional_params", - "prometheus_expose_params": "prometheus_expose_additional_params", - "fastapi_logging_middleware_enabled": "logging_access_log_enabled", - } - prometheus_instrumentator_params: dict[str, typing.Any] | UnsetType = dataclasses.field( - default=UNSET, repr=False, compare=False - ) - prometheus_instrument_params: dict[str, typing.Any] | UnsetType = dataclasses.field( - default=UNSET, repr=False, compare=False - ) - prometheus_expose_params: dict[str, typing.Any] | UnsetType = dataclasses.field( - default=UNSET, repr=False, compare=False - ) - fastapi_logging_middleware_enabled: bool | UnsetType = dataclasses.field(default=UNSET, repr=False, compare=False) def __post_init__(self) -> None: # @dataclass(slots=True) replaces the class object, breaking bare super(). diff --git a/lite_bootstrap/bootstrappers/fastmcp_bootstrapper.py b/lite_bootstrap/bootstrappers/fastmcp_bootstrapper.py index 5e168b5..79047b5 100644 --- a/lite_bootstrap/bootstrappers/fastmcp_bootstrapper.py +++ b/lite_bootstrap/bootstrappers/fastmcp_bootstrapper.py @@ -20,7 +20,6 @@ disable_sentry_integrations, warn_about_sentry_integrations_installed_earlier, ) -from lite_bootstrap.types import UNSET, UnsetType if import_checker.is_fastmcp_installed: @@ -214,19 +213,6 @@ class FastMcpConfig( prometheus_instrumentator_additional_params: dict[str, typing.Any] = dataclasses.field(default_factory=dict) prometheus_instrument_additional_params: dict[str, typing.Any] = dataclasses.field(default_factory=dict) _sentry_lean_replaced_integrations: typing.ClassVar[Mapping[str, str]] = {"starlette": "StarletteIntegration"} - # Deprecated names, removed in 2.0. - _renamed_fields: typing.ClassVar[Mapping[str, str]] = { - "fastmcp_logging_middleware_enabled": "logging_access_log_enabled", - "prometheus_instrumentator_params": "prometheus_instrumentator_additional_params", - "prometheus_instrument_params": "prometheus_instrument_additional_params", - } - fastmcp_logging_middleware_enabled: bool | UnsetType = dataclasses.field(default=UNSET, repr=False, compare=False) - prometheus_instrumentator_params: dict[str, typing.Any] | UnsetType = dataclasses.field( - default=UNSET, repr=False, compare=False - ) - prometheus_instrument_params: dict[str, typing.Any] | UnsetType = dataclasses.field( - default=UNSET, repr=False, compare=False - ) @dataclasses.dataclass(kw_only=True) diff --git a/lite_bootstrap/bootstrappers/free_bootstrapper.py b/lite_bootstrap/bootstrappers/free_bootstrapper.py index 302ae16..2da2fd6 100644 --- a/lite_bootstrap/bootstrappers/free_bootstrapper.py +++ b/lite_bootstrap/bootstrappers/free_bootstrapper.py @@ -2,7 +2,6 @@ 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 @@ -33,9 +32,3 @@ def __init__(self, bootstrap_config: FreeConfig) -> None: def _prepare_application(self) -> None: return None - - -if typing.TYPE_CHECKING: - FreeBootstrapperConfig = FreeConfig - -__getattr__ = deprecated_module_getattr(__name__, {"FreeBootstrapperConfig": (FreeConfig, "FreeConfig")}) diff --git a/lite_bootstrap/bootstrappers/litestar_bootstrapper.py b/lite_bootstrap/bootstrappers/litestar_bootstrapper.py index eb1e26f..4857abd 100644 --- a/lite_bootstrap/bootstrappers/litestar_bootstrapper.py +++ b/lite_bootstrap/bootstrappers/litestar_bootstrapper.py @@ -28,7 +28,6 @@ warn_about_sentry_integrations_installed_earlier, ) from lite_bootstrap.instruments.swagger_instrument import SwaggerConfig, SwaggerInstrument -from lite_bootstrap.types import UNSET, UnsetType if typing.TYPE_CHECKING: @@ -262,16 +261,7 @@ class LitestarConfig( # Bounds path-label cardinality (Litestar defaults False -> raw URLs leak memory). See litestar#4891. prometheus_group_path: bool = True swagger_additional_params: dict[str, typing.Any] = dataclasses.field(default_factory=dict) - # Deprecated names, removed in 2.0. _sentry_lean_replaced_integrations: typing.ClassVar[Mapping[str, str]] = {"litestar": "LitestarIntegration"} - _renamed_fields: typing.ClassVar[Mapping[str, str]] = { - "litestar_logging_middleware_enabled": "logging_access_log_enabled", - "swagger_extra_params": "swagger_additional_params", - } - litestar_logging_middleware_enabled: bool | UnsetType = dataclasses.field(default=UNSET, repr=False, compare=False) - swagger_extra_params: dict[str, typing.Any] | UnsetType = dataclasses.field( - default=UNSET, repr=False, compare=False - ) def __post_init__(self) -> None: # @dataclass(slots=True) replaces the class object, breaking bare super(). diff --git a/lite_bootstrap/helpers/warn.py b/lite_bootstrap/helpers/warn.py index 76fc2c7..ca78654 100644 --- a/lite_bootstrap/helpers/warn.py +++ b/lite_bootstrap/helpers/warn.py @@ -6,6 +6,7 @@ _DATACLASS_GENERATED_INIT: typing.Final = "" _INTERNAL_MODULES: typing.Final = frozenset({__name__.split(".")[0], "dataclasses"}) +NEXT_MAJOR_VERSION: typing.Final = "3.0" def _is_internal_frame(frame: types.FrameType) -> bool: @@ -52,7 +53,7 @@ def __getattr__(name: str) -> object: # noqa: N807 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}.", + f"{name} is deprecated and will be removed in lite-bootstrap {NEXT_MAJOR_VERSION}; {advice}.", category=DeprecationWarning, ) return target diff --git a/lite_bootstrap/instruments/base.py b/lite_bootstrap/instruments/base.py index c9d4166..3e619f3 100644 --- a/lite_bootstrap/instruments/base.py +++ b/lite_bootstrap/instruments/base.py @@ -3,7 +3,7 @@ from collections.abc import Mapping from lite_bootstrap.exceptions import ConfigurationError -from lite_bootstrap.helpers.warn import warn_at_caller +from lite_bootstrap.helpers.warn import NEXT_MAJOR_VERSION, warn_at_caller from lite_bootstrap.types import UNSET, UnsetType @@ -40,7 +40,8 @@ def __post_init__(self) -> None: msg = f"{old_name} and {new_name} are both set; {old_name} is deprecated, set only {new_name}." raise ConfigurationError(msg) warn_at_caller( - f"{old_name} is deprecated and will be removed in lite-bootstrap 2.0; use {new_name} instead.", + f"{old_name} is deprecated and will be removed in lite-bootstrap {NEXT_MAJOR_VERSION}; " + f"use {new_name} instead.", category=DeprecationWarning, ) object.__setattr__(self, new_name, old_value) @@ -63,31 +64,12 @@ def from_object(cls, obj: object) -> typing.Self: ConfigT = typing.TypeVar("ConfigT", bound=BaseConfig) -# Legacy name -> current name; __init_subclass__ forwards an out-of-tree instrument's old spelling. -_RENAMED_ATTRIBUTES: typing.Final = { - "not_ready_message": "not_configured_reason", - "check_dependencies": "dependencies_installed", -} - - @dataclasses.dataclass(kw_only=True, slots=True) class BaseInstrument(typing.Generic[ConfigT]): bootstrap_config: ConfigT not_configured_reason = "" missing_dependency_message = "" - def __init_subclass__(cls, **kwargs: object) -> None: - # @dataclass(slots=True) replaces the class object, breaking bare super(). - 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: ... def teardown(self) -> None: ... diff --git a/lite_bootstrap/instruments/logging_factory.py b/lite_bootstrap/instruments/logging_factory.py index f4f7702..8ce9da3 100644 --- a/lite_bootstrap/instruments/logging_factory.py +++ b/lite_bootstrap/instruments/logging_factory.py @@ -6,39 +6,12 @@ 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] - - -class _AddressProtocol(typing.Protocol): - host: str - port: int - - -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 @@ -163,6 +136,3 @@ def close_handlers(self) -> None: target.close() self._created_handlers.clear() self._leveled_loggers.clear() - - -__getattr__ = deprecated_module_getattr(__name__, _DEPRECATED_PROTOCOLS) diff --git a/lite_bootstrap/instruments/logging_instrument.py b/lite_bootstrap/instruments/logging_instrument.py index f54a0fc..e9182f0 100644 --- a/lite_bootstrap/instruments/logging_instrument.py +++ b/lite_bootstrap/instruments/logging_instrument.py @@ -5,14 +5,9 @@ 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 ( - _DEPRECATED_PROTOCOLS, - _AddressProtocol, _MemoryLoggerFactoryConfig, - _RequestProtocol, - _ScopeType, _serialize_log_to_string, ) @@ -239,11 +234,3 @@ 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) diff --git a/lite_bootstrap/instruments/opentelemetry_instrument.py b/lite_bootstrap/instruments/opentelemetry_instrument.py index 82567ca..cd2fe8d 100644 --- a/lite_bootstrap/instruments/opentelemetry_instrument.py +++ b/lite_bootstrap/instruments/opentelemetry_instrument.py @@ -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 deprecated_module_getattr, warn_at_caller +from lite_bootstrap.helpers.warn import warn_at_caller from lite_bootstrap.instruments.base import BaseConfig, BaseInstrument @@ -387,9 +387,3 @@ def teardown(self) -> None: self._meter_provider.shutdown() finally: self._meter_provider = None - - -if typing.TYPE_CHECKING: - OpentelemetryConfig = OpenTelemetryConfig - -__getattr__ = deprecated_module_getattr(__name__, {"OpentelemetryConfig": (OpenTelemetryConfig, "OpenTelemetryConfig")}) diff --git a/lite_bootstrap/instruments/sentry_instrument.py b/lite_bootstrap/instruments/sentry_instrument.py index 248f953..8b596a1 100644 --- a/lite_bootstrap/instruments/sentry_instrument.py +++ b/lite_bootstrap/instruments/sentry_instrument.py @@ -4,9 +4,9 @@ from collections.abc import Mapping from lite_bootstrap import import_checker -from lite_bootstrap.helpers.warn import deprecated_module_getattr, warn_at_caller +from lite_bootstrap.helpers.warn import warn_at_caller from lite_bootstrap.instruments.base import BaseConfig, BaseInstrument -from lite_bootstrap.instruments.logging_factory import STRUCTLOG_META_KEYS, StructuredLogPayload +from lite_bootstrap.instruments.logging_factory import StructuredLogPayload if typing.TYPE_CHECKING: @@ -269,12 +269,3 @@ 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")}, -) diff --git a/tests/instruments/test_base_instrument.py b/tests/instruments/test_base_instrument.py index 5a62417..922028f 100644 --- a/tests/instruments/test_base_instrument.py +++ b/tests/instruments/test_base_instrument.py @@ -1,7 +1,3 @@ -import dataclasses - -import pytest - from lite_bootstrap.instruments.base import BaseConfig, BaseInstrument @@ -10,47 +6,3 @@ def test_base_instrument_defaults() -> None: assert BaseInstrument.missing_dependency_message == "" assert BaseInstrument.dependencies_installed() is True assert BaseInstrument.is_configured(BaseConfig()) is True - - -def test_an_instrument_written_against_the_old_attribute_names_still_works() -> None: - """INVARIANT: a subclass declaring not_ready_message or check_dependencies keeps both behaviours. - - Both names are part of the instrument protocol documented in `docs/introduction/configuration.md`, - so an out-of-tree instrument may define either. What breaks it is renaming them without the - forwarding in `BaseInstrument.__init_subclass__`: the bootstrapper would read the inherited empty - `not_configured_reason` and report no skip reason, and — far worse — it would call the inherited - `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" - - @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: - @dataclasses.dataclass(kw_only=True, slots=True) - class BothNamesInstrument(BaseInstrument[BaseConfig]): - not_ready_message = "legacy reason" - not_configured_reason = "current reason" - - assert BothNamesInstrument.not_configured_reason == "current reason" diff --git a/tests/test_deprecated_aliases.py b/tests/test_deprecated_aliases.py index 1cf59dc..3508e58 100644 --- a/tests/test_deprecated_aliases.py +++ b/tests/test_deprecated_aliases.py @@ -1,62 +1,46 @@ import types -import warnings import pytest import lite_bootstrap from lite_bootstrap.bootstrappers import free_bootstrapper +from lite_bootstrap.helpers.warn import deprecated_module_getattr from lite_bootstrap.instruments import logging_factory, logging_instrument, opentelemetry_instrument, sentry_instrument -# Tests elsewhere reload instrument modules, so a replacement is looked up when the test runs. -DEPRECATED_ALIASES = [ - (opentelemetry_instrument, "OpentelemetryConfig", opentelemetry_instrument, "OpenTelemetryConfig"), - (free_bootstrapper, "FreeBootstrapperConfig", free_bootstrapper, "FreeConfig"), - (lite_bootstrap, "FreeBootstrapperConfig", free_bootstrapper, "FreeConfig"), - (sentry_instrument, "IGNORED_STRUCTLOG_ATTRIBUTES", logging_factory, "STRUCTLOG_META_KEYS"), +REMOVED_PROTOCOLS = ["AddressProtocol", "RequestProtocol", "ScopeType"] +NAMES_REMOVED_IN_2_0 = [ + (opentelemetry_instrument, "OpentelemetryConfig"), + (free_bootstrapper, "FreeBootstrapperConfig"), + (lite_bootstrap, "FreeBootstrapperConfig"), + (sentry_instrument, "IGNORED_STRUCTLOG_ATTRIBUTES"), + *[(module, name) for module in (logging_factory, logging_instrument) for name in REMOVED_PROTOCOLS], ] -DEPRECATED_PROTOCOLS = ["AddressProtocol", "RequestProtocol", "ScopeType"] -MODULES_WITH_DEPRECATIONS = [ - lite_bootstrap, - free_bootstrapper, - logging_factory, - logging_instrument, - opentelemetry_instrument, - sentry_instrument, -] - +TARGET = object() +module_getattr = deprecated_module_getattr("some.module", {"OldName": (TARGET, "NewName"), "GoneName": (TARGET, None)}) -@pytest.mark.parametrize(("module", "name", "replacement_module", "replacement"), DEPRECATED_ALIASES) -def test_a_deprecated_alias_warns_and_still_resolves( - module: types.ModuleType, - name: str, - replacement_module: types.ModuleType, - replacement: str, -) -> None: - with pytest.warns(DeprecationWarning, match=f"{name} is deprecated.*; use {replacement} instead") as record: - resolved = getattr(module, name) - assert resolved is getattr(replacement_module, replacement) - assert record[0].filename == __file__ +@pytest.mark.parametrize(("module", "name"), NAMES_REMOVED_IN_2_0) +def test_a_name_deprecated_in_1_x_is_gone(module: types.ModuleType, name: str) -> None: + with pytest.raises(AttributeError, match=name): + getattr(module, name) -@pytest.mark.parametrize("module", [logging_factory, logging_instrument]) -@pytest.mark.parametrize("name", DEPRECATED_PROTOCOLS) -def test_an_unused_logging_protocol_warns_and_still_resolves(module: types.ModuleType, name: str) -> None: - with pytest.warns(DeprecationWarning, match=f"{name} is deprecated.*; nothing replaces it"): - resolved = getattr(module, name) +def test_a_deprecated_alias_warns_and_still_resolves() -> None: + with pytest.warns( + DeprecationWarning, match=r"OldName is deprecated.* lite-bootstrap 3\.0; use NewName instead" + ) as record: + resolved = module_getattr("OldName") - assert resolved is not None + assert resolved is TARGET + assert record[0].filename == __file__ -@pytest.mark.parametrize("module", MODULES_WITH_DEPRECATIONS) -def test_an_unknown_attribute_still_raises_attribute_error(module: types.ModuleType) -> None: - with pytest.raises(AttributeError, match="no_such_name"): - _ = module.no_such_name +def test_a_deprecated_alias_without_a_replacement_says_so() -> None: + with pytest.warns(DeprecationWarning, match="GoneName is deprecated.*; nothing replaces it"): + assert module_getattr("GoneName") is TARGET -@pytest.mark.parametrize("module", MODULES_WITH_DEPRECATIONS) -def test_a_star_import_does_not_warn(module: types.ModuleType) -> None: - with warnings.catch_warnings(): - warnings.simplefilter("error", DeprecationWarning) - exec(f"from {module.__name__} import *", {}) # noqa: S102 +def test_an_unknown_attribute_still_raises_attribute_error() -> None: + with pytest.raises(AttributeError, match=r"module 'some\.module' has no attribute 'no_such_name'"): + module_getattr("no_such_name") diff --git a/tests/test_renamed_config_fields.py b/tests/test_renamed_config_fields.py index d74d1f5..d81181c 100644 --- a/tests/test_renamed_config_fields.py +++ b/tests/test_renamed_config_fields.py @@ -1,78 +1,76 @@ import dataclasses import types import typing +from collections.abc import Mapping import pytest from lite_bootstrap import ConfigurationError, FastAPIConfig, FastMcpConfig, LitestarConfig from lite_bootstrap.instruments.base import BaseConfig +from lite_bootstrap.types import UNSET, UnsetType PARAMS = {"key": "value"} -RENAMED_FIELDS = [ - (FastAPIConfig, "prometheus_instrumentator_params", "prometheus_instrumentator_additional_params", PARAMS), - (FastAPIConfig, "prometheus_instrument_params", "prometheus_instrument_additional_params", PARAMS), - (FastAPIConfig, "prometheus_expose_params", "prometheus_expose_additional_params", PARAMS), - (FastAPIConfig, "fastapi_logging_middleware_enabled", "logging_access_log_enabled", True), - (FastMcpConfig, "prometheus_instrumentator_params", "prometheus_instrumentator_additional_params", PARAMS), - (FastMcpConfig, "prometheus_instrument_params", "prometheus_instrument_additional_params", PARAMS), - (FastMcpConfig, "fastmcp_logging_middleware_enabled", "logging_access_log_enabled", True), - (LitestarConfig, "swagger_extra_params", "swagger_additional_params", PARAMS), - (LitestarConfig, "litestar_logging_middleware_enabled", "logging_access_log_enabled", True), +FIELDS_REMOVED_IN_2_0 = [ + (FastAPIConfig, "prometheus_instrumentator_params"), + (FastAPIConfig, "prometheus_instrument_params"), + (FastAPIConfig, "prometheus_expose_params"), + (FastAPIConfig, "fastapi_logging_middleware_enabled"), + (FastMcpConfig, "prometheus_instrumentator_params"), + (FastMcpConfig, "prometheus_instrument_params"), + (FastMcpConfig, "fastmcp_logging_middleware_enabled"), + (LitestarConfig, "swagger_extra_params"), + (LitestarConfig, "litestar_logging_middleware_enabled"), ] -@pytest.mark.parametrize(("config_class", "old_name", "new_name", "value"), RENAMED_FIELDS) -def test_an_old_field_name_sets_the_new_field_and_warns( - config_class: type[BaseConfig], - old_name: str, - new_name: str, - value: object, -) -> None: - kwargs: dict[str, typing.Any] = {old_name: value} - with pytest.warns(DeprecationWarning, match=f"{old_name} is deprecated.*; use {new_name} instead") as record: - config = config_class(**kwargs) +@dataclasses.dataclass(kw_only=True, slots=True, frozen=True) +class RenamingConfig(BaseConfig): + new_params: dict[str, typing.Any] = dataclasses.field(default_factory=dict) + _renamed_fields: typing.ClassVar[Mapping[str, str]] = {"old_params": "new_params"} + old_params: dict[str, typing.Any] | UnsetType = dataclasses.field(default=UNSET, repr=False, compare=False) - assert getattr(config, new_name) == value + +@pytest.mark.parametrize(("config_class", "old_name"), FIELDS_REMOVED_IN_2_0) +def test_a_field_name_deprecated_in_1_x_is_rejected(config_class: type[BaseConfig], old_name: str) -> None: + kwargs: dict[str, typing.Any] = {old_name: PARAMS} + with pytest.raises(TypeError, match=old_name): + config_class(**kwargs) + + +def test_an_old_field_name_sets_the_new_field_and_warns() -> None: + with pytest.warns( + DeprecationWarning, match=r"old_params is deprecated.* lite-bootstrap 3\.0; use new_params instead" + ) as record: + config = RenamingConfig(old_params=PARAMS) + + assert config.new_params == PARAMS assert record[0].filename == __file__ -@pytest.mark.parametrize(("config_class", "old_name", "new_name", "value"), RENAMED_FIELDS) -def test_setting_an_old_and_a_new_field_name_together_raises( - config_class: type[BaseConfig], - old_name: str, - new_name: str, - value: object, -) -> None: - kwargs: dict[str, typing.Any] = {old_name: value, new_name: value} - with pytest.raises(ConfigurationError, match=f"{old_name}.*{new_name}"): - config_class(**kwargs) +def test_setting_an_old_and_a_new_field_name_together_raises() -> None: + with pytest.raises(ConfigurationError, match=r"old_params.*new_params"): + RenamingConfig(old_params=PARAMS, new_params=PARAMS) -@pytest.mark.parametrize(("config_class", "old_name", "new_name", "value"), RENAMED_FIELDS) -def test_from_dict_accepts_an_old_field_name( - config_class: type[BaseConfig], - old_name: str, - new_name: str, - value: object, -) -> None: - with pytest.warns(DeprecationWarning, match=old_name): - config = config_class.from_dict({old_name: value}) +def test_from_dict_accepts_an_old_field_name() -> None: + with pytest.warns(DeprecationWarning, match="old_params"): + config = RenamingConfig.from_dict({"old_params": PARAMS}) - assert getattr(config, new_name) == value + assert config.new_params == PARAMS def test_from_object_with_an_old_and_a_new_field_name_raises() -> None: - settings = types.SimpleNamespace(swagger_extra_params=PARAMS, swagger_additional_params={"other": "value"}) + settings = types.SimpleNamespace(old_params=PARAMS, new_params={"other": "value"}) - with pytest.raises(ConfigurationError, match=r"swagger_extra_params.*swagger_additional_params"): - LitestarConfig.from_object(settings) + with pytest.raises(ConfigurationError, match=r"old_params.*new_params"): + RenamingConfig.from_object(settings) def test_a_config_built_from_an_old_field_name_can_be_replaced() -> None: - with pytest.warns(DeprecationWarning, match="swagger_extra_params"): - config = LitestarConfig(swagger_extra_params=PARAMS) + with pytest.warns(DeprecationWarning, match="old_params"): + config = RenamingConfig(old_params=PARAMS) - replaced = dataclasses.replace(config, swagger_additional_params={"other": "value"}) + replaced = dataclasses.replace(config, new_params={"other": "value"}) - assert replaced.swagger_additional_params == {"other": "value"} + assert replaced.new_params == {"other": "value"}