Skip to content

Commit d06e8c4

Browse files
committed
fix: restore deprecation shims and move type_encoders to serialization plugin
Two corrections to the serializer refactor on this branch: 1. Restore advanced_alchemy/_serialization.py and advanced_alchemy/service/typing.py as deprecation shims. Both modules are released in v1.9.x with substantial public surface (BaseModel, schema_dump, type guards, encode/decode_json, the *_INSTALLED flags, etc.) so deleting them outright is a breaking change. The shims re- export every name from its new home (advanced_alchemy.utils.serialization, advanced_alchemy.utils.serializers, advanced_alchemy.typing) and emit a DeprecationWarning via the project's warn_deprecation helper, scheduled for removal in 2.0. 2. Move the Litestar type_encoders / type_decoders wiring from SQLAlchemyInitPlugin.on_app_init to SQLAlchemySerializationPlugin — serialization registration is a serialization-plugin concern. The serialization plugin now implements both SerializationPlugin and InitPluginProtocol so AA's built-in encoders are still picked up transparently when SQLAlchemyPlugin is used. Add tests covering: every shim name resolves to the canonical object and emits a DeprecationWarning; PYDANTIC_USE_FAILFAST is preserved without a warning; the init plugin no longer touches type_encoders; the serialization plugin merges AA encoders/decoders with user precedence; and the serialization plugin satisfies both Litestar protocols.
1 parent e888e39 commit d06e8c4

6 files changed

Lines changed: 521 additions & 81 deletions

File tree

advanced_alchemy/_serialization.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
# pyright: reportUnsupportedDunderAll=false
2+
"""Deprecated shim for ``advanced_alchemy._serialization``.
3+
4+
Re-exports the public surface that lived here in v1.x from its new
5+
locations under :mod:`advanced_alchemy.utils.serialization` and
6+
:mod:`advanced_alchemy.typing`. Importing any name from this module
7+
emits a :class:`DeprecationWarning` via the project's
8+
:func:`~advanced_alchemy.utils.deprecation.warn_deprecation` helper.
9+
10+
This module will be removed in 2.0.
11+
"""
12+
13+
from typing import Any
14+
15+
from advanced_alchemy.utils.deprecation import warn_deprecation
16+
17+
_RENAMES: "dict[str, tuple[str, str]]" = {
18+
"BaseModel": ("advanced_alchemy.typing", "BaseModel"),
19+
"PYDANTIC_INSTALLED": ("advanced_alchemy.typing", "PYDANTIC_INSTALLED"),
20+
"encode_json": ("advanced_alchemy.utils.serialization", "encode_json"),
21+
"decode_json": ("advanced_alchemy.utils.serialization", "decode_json"),
22+
"encode_complex_type": ("advanced_alchemy.utils.serialization", "encode_complex_type"),
23+
"decode_complex_type": ("advanced_alchemy.utils.serialization", "decode_complex_type"),
24+
"convert_datetime_to_gmt_iso": ("advanced_alchemy.utils.serialization", "convert_datetime_to_gmt_iso"),
25+
"convert_date_to_iso": ("advanced_alchemy.utils.serialization", "convert_date_to_iso"),
26+
}
27+
28+
# Names below are resolved lazily via ``__getattr__`` for the deprecation
29+
# warning. ruff's F822 / pyright's reportUnsupportedDunderAll don't see them
30+
# at module level — that's the point.
31+
__all__ = ( # noqa: F822
32+
"PYDANTIC_INSTALLED",
33+
"BaseModel",
34+
"convert_date_to_iso",
35+
"convert_datetime_to_gmt_iso",
36+
"decode_complex_type",
37+
"decode_json",
38+
"encode_complex_type",
39+
"encode_json",
40+
)
41+
42+
43+
def __getattr__(name: str) -> Any:
44+
if name not in _RENAMES:
45+
msg = f"module {__name__!r} has no attribute {name!r}"
46+
raise AttributeError(msg)
47+
new_module, new_name = _RENAMES[name]
48+
warn_deprecation(
49+
version="1.10.0",
50+
removal_in="2.0.0",
51+
deprecated_name=f"{__name__}.{name}",
52+
kind="import",
53+
alternative=f"{new_module}.{new_name}",
54+
)
55+
import importlib
56+
57+
return getattr(importlib.import_module(new_module), new_name)

advanced_alchemy/extensions/litestar/plugins/init/plugin.py

Lines changed: 0 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -34,11 +34,8 @@
3434
StatementTypeT,
3535
)
3636
from advanced_alchemy.service import ModelDictListT, ModelDictT, ModelDTOT, ModelOrRowMappingT, ModelT, OffsetPagination
37-
from advanced_alchemy.utils.serialization import DEFAULT_TYPE_ENCODERS
3837

3938
if TYPE_CHECKING:
40-
from collections.abc import Callable
41-
4239
from click import Group
4340
from litestar.config.app import AppConfig
4441
from litestar.types import BeforeMessageSendHookHandler
@@ -48,55 +45,6 @@
4845
__all__ = ("SQLAlchemyInitPlugin",)
4946

5047

51-
def _get_aa_type_encoders() -> dict[type, "Callable[[Any], Any]"]:
52-
"""Get Advanced Alchemy's built-in type encoders.
53-
54-
These encoders handle database-specific types that need special
55-
serialization. They are added to Litestar's type_encoders with
56-
lower precedence than user-defined encoders.
57-
58-
Returns:
59-
Dictionary of type to encoder function mappings.
60-
"""
61-
encoders: dict[type, Callable[[Any], Any]] = {**DEFAULT_TYPE_ENCODERS}
62-
63-
# asyncpg UUID type (PostgreSQL asyncpg driver)
64-
with contextlib.suppress(ImportError):
65-
from asyncpg.pgproto import pgproto # pyright: ignore[reportMissingImports]
66-
67-
encoders[pgproto.UUID] = str
68-
69-
# uuid_utils UUID type (fast UUID implementation)
70-
with contextlib.suppress(ImportError):
71-
import uuid_utils # pyright: ignore[reportMissingImports]
72-
73-
encoders[uuid_utils.UUID] = str # pyright: ignore[reportUnknownMemberType]
74-
75-
return encoders
76-
77-
78-
def _get_aa_type_decoders() -> list[tuple["Callable[[Any], bool]", "Callable[[type, Any], Any]"]]:
79-
"""Get Advanced Alchemy's built-in type decoders.
80-
81-
These decoders handle database-specific types that need special
82-
deserialization during request parsing.
83-
84-
Returns:
85-
List of (predicate, decoder) tuples for Litestar's type_decoders.
86-
"""
87-
decoders: list[tuple[Callable[[Any], bool], Callable[[type, Any], Any]]] = []
88-
89-
# uuid_utils UUID type decoder
90-
with contextlib.suppress(ImportError):
91-
import uuid_utils # pyright: ignore[reportMissingImports]
92-
93-
decoders.append(
94-
(lambda x: x is uuid_utils.UUID, lambda t, v: t(str(v))) # pyright: ignore[reportUnknownMemberType]
95-
)
96-
97-
return decoders
98-
99-
10048
signature_namespace_values: dict[str, Any] = {
10149
"BeforeAfter": BeforeAfter,
10250
"OnBeforeAfter": OnBeforeAfter,
@@ -179,15 +127,6 @@ def on_app_init(self, app_config: "AppConfig") -> "AppConfig":
179127
"""
180128
self._validate_config()
181129

182-
# Add AA built-in type encoders/decoders
183-
# These are added BEFORE user encoders so user config takes precedence
184-
aa_encoders = _get_aa_type_encoders()
185-
aa_decoders = _get_aa_type_decoders()
186-
187-
# Merge: AA built-ins first, then user encoders override
188-
app_config.type_encoders = {**aa_encoders, **(app_config.type_encoders or {})}
189-
app_config.type_decoders = [*aa_decoders, *(app_config.type_decoders or [])]
190-
191130
with contextlib.suppress(ImportError):
192131
from asyncpg.pgproto import pgproto # pyright: ignore[reportMissingImports]
193132

advanced_alchemy/extensions/litestar/plugins/serialization.py

Lines changed: 64 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,78 @@
1-
from typing import Any
1+
import contextlib
2+
from typing import TYPE_CHECKING, Any
23

3-
from litestar.plugins import SerializationPlugin
4+
from litestar.plugins import InitPluginProtocol, SerializationPlugin
45
from litestar.typing import FieldDefinition
56
from sqlalchemy.orm import DeclarativeBase
67

78
from advanced_alchemy.extensions.litestar.dto import SQLAlchemyDTO
89
from advanced_alchemy.extensions.litestar.plugins import _slots_base
10+
from advanced_alchemy.utils.serialization import DEFAULT_TYPE_ENCODERS
911

12+
if TYPE_CHECKING:
13+
from collections.abc import Callable
1014

11-
class SQLAlchemySerializationPlugin(SerializationPlugin, _slots_base.SlotsBase):
15+
from litestar.config.app import AppConfig
16+
17+
18+
def _get_aa_type_encoders() -> "dict[type, Callable[[Any], Any]]":
19+
"""Return Advanced Alchemy's built-in Litestar type encoders.
20+
21+
These cover database-specific types (asyncpg's ``pgproto.UUID``,
22+
``uuid_utils.UUID``) that need explicit serialization to JSON-friendly
23+
forms. They are merged into ``AppConfig.type_encoders`` with lower
24+
precedence than user-supplied encoders.
25+
"""
26+
encoders: dict[type, Callable[[Any], Any]] = {**DEFAULT_TYPE_ENCODERS}
27+
28+
with contextlib.suppress(ImportError):
29+
from asyncpg.pgproto import pgproto # pyright: ignore[reportMissingImports]
30+
31+
encoders[pgproto.UUID] = str
32+
33+
with contextlib.suppress(ImportError):
34+
import uuid_utils # pyright: ignore[reportMissingImports]
35+
36+
encoders[uuid_utils.UUID] = str # pyright: ignore[reportUnknownMemberType]
37+
38+
return encoders
39+
40+
41+
def _get_aa_type_decoders() -> "list[tuple[Callable[[Any], bool], Callable[[type, Any], Any]]]":
42+
"""Return Advanced Alchemy's built-in Litestar type decoders.
43+
44+
Currently covers ``uuid_utils.UUID`` for request-side parsing. Decoders
45+
are merged into ``AppConfig.type_decoders`` with lower precedence than
46+
user-supplied decoders.
47+
"""
48+
decoders: list[tuple[Callable[[Any], bool], Callable[[type, Any], Any]]] = []
49+
50+
with contextlib.suppress(ImportError):
51+
import uuid_utils # pyright: ignore[reportMissingImports]
52+
53+
decoders.append(
54+
(lambda x: x is uuid_utils.UUID, lambda t, v: t(str(v))) # pyright: ignore[reportUnknownMemberType]
55+
)
56+
57+
return decoders
58+
59+
60+
class SQLAlchemySerializationPlugin(SerializationPlugin, InitPluginProtocol, _slots_base.SlotsBase):
1261
def __init__(self) -> None:
1362
self._type_dto_map: dict[type[DeclarativeBase], type[SQLAlchemyDTO[Any]]] = {}
1463

64+
def on_app_init(self, app_config: "AppConfig") -> "AppConfig":
65+
"""Register Advanced Alchemy's built-in type encoders and decoders.
66+
67+
AA encoders/decoders are added with lower precedence so user-supplied
68+
``type_encoders`` / ``type_decoders`` on the application config win.
69+
"""
70+
aa_encoders = _get_aa_type_encoders()
71+
aa_decoders = _get_aa_type_decoders()
72+
app_config.type_encoders = {**aa_encoders, **(app_config.type_encoders or {})}
73+
app_config.type_decoders = [*aa_decoders, *(app_config.type_decoders or [])]
74+
return app_config
75+
1576
def supports_type(self, field_definition: FieldDefinition) -> bool:
1677
return (
1778
field_definition.is_collection and field_definition.has_inner_subclass_of(DeclarativeBase)

0 commit comments

Comments
 (0)