Skip to content

Commit b1fea84

Browse files
authored
feat: attrs integration (#503)
Adds `attrs` support into the `ResultConverter` mixin. This enables `to_schema` and `schema_dump` to natively understand `attrs`.
1 parent 0b2749e commit b1fea84

File tree

13 files changed

+1467
-127
lines changed

13 files changed

+1467
-127
lines changed

advanced_alchemy/service/__init__.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,18 @@
2222
from advanced_alchemy.service._util import ResultConverter, find_filter
2323
from advanced_alchemy.service.pagination import OffsetPagination
2424
from advanced_alchemy.service.typing import (
25+
ATTRS_INSTALLED,
26+
AttrsInstance,
2527
FilterTypeT,
2628
ModelDictListT,
2729
ModelDictT,
2830
ModelDTOT,
2931
SupportedSchemaModel,
32+
fields,
33+
is_attrs_instance,
34+
is_attrs_instance_with_field,
35+
is_attrs_instance_without_field,
36+
is_attrs_schema,
3037
is_dict,
3138
is_dict_with_field,
3239
is_dict_without_field,
@@ -47,7 +54,9 @@
4754
)
4855

4956
__all__ = (
57+
"ATTRS_INSTALLED",
5058
"DEFAULT_ERROR_MESSAGE_TEMPLATES",
59+
"AttrsInstance",
5160
"Empty",
5261
"EmptyType",
5362
"ErrorMessages",
@@ -68,7 +77,12 @@
6877
"SQLAlchemySyncRepositoryReadService",
6978
"SQLAlchemySyncRepositoryService",
7079
"SupportedSchemaModel",
80+
"fields",
7181
"find_filter",
82+
"is_attrs_instance",
83+
"is_attrs_instance_with_field",
84+
"is_attrs_instance_without_field",
85+
"is_attrs_schema",
7286
"is_dict",
7387
"is_dict_with_field",
7488
"is_dict_without_field",

advanced_alchemy/service/_async.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@
3030
BulkModelDictT,
3131
ModelDictListT,
3232
ModelDictT,
33+
asdict,
34+
is_attrs_instance,
3335
is_dict,
3436
is_dto_data,
3537
is_msgspec_struct,
@@ -455,6 +457,20 @@ async def to_model(
455457

456458
if is_dto_data(data):
457459
return cast("ModelT", data.create_instance())
460+
461+
if is_attrs_instance(data):
462+
return model_from_dict(
463+
model=self.model_type,
464+
**asdict(data),
465+
)
466+
467+
# Fallback for objects with __dict__ (e.g., regular classes)
468+
if hasattr(data, "__dict__"):
469+
return model_from_dict(
470+
model=self.model_type,
471+
**data.__dict__,
472+
)
473+
458474
return cast("ModelT", data)
459475

460476
async def list_and_count(

advanced_alchemy/service/_sync.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@
2929
BulkModelDictT,
3030
ModelDictListT,
3131
ModelDictT,
32+
asdict,
33+
is_attrs_instance,
3234
is_dict,
3335
is_dto_data,
3436
is_msgspec_struct,
@@ -454,6 +456,20 @@ def to_model(
454456

455457
if is_dto_data(data):
456458
return cast("ModelT", data.create_instance())
459+
460+
if is_attrs_instance(data):
461+
return model_from_dict(
462+
model=self.model_type,
463+
**asdict(data),
464+
)
465+
466+
# Fallback for objects with __dict__ (e.g., regular classes)
467+
if hasattr(data, "__dict__"):
468+
return model_from_dict(
469+
model=self.model_type,
470+
**data.__dict__,
471+
)
472+
457473
return cast("ModelT", data)
458474

459475
def list_and_count(

advanced_alchemy/service/_typing.py

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,16 +126,74 @@ def as_builtins(self) -> Any:
126126

127127
LITESTAR_INSTALLED = False # pyright: ignore[reportConstantRedefinition]
128128

129+
try:
130+
from attrs import AttrsInstance, asdict, define, field, fields, has # pyright: ignore
131+
132+
ATTRS_INSTALLED = True
133+
except ImportError:
134+
135+
@runtime_checkable
136+
class AttrsInstance(Protocol): # type: ignore[no-redef]
137+
"""Placeholder Implementation for attrs classes"""
138+
139+
def asdict(*args: Any, **kwargs: Any) -> "dict[str, Any]": # type: ignore[misc] # noqa: ARG001
140+
"""Placeholder implementation"""
141+
return {}
142+
143+
def define(*args: Any, **kwargs: Any) -> Any: # type: ignore[no-redef] # noqa: ARG001
144+
"""Placeholder implementation"""
145+
return lambda cls: cls # pyright: ignore[reportUnknownVariableType,reportUnknownLambdaType]
146+
147+
def field(*args: Any, **kwargs: Any) -> Any: # type: ignore[no-redef] # noqa: ARG001
148+
"""Placeholder implementation"""
149+
return None
150+
151+
def fields(*args: Any, **kwargs: Any) -> "tuple[Any, ...]": # type: ignore[misc] # noqa: ARG001
152+
"""Placeholder implementation"""
153+
return ()
154+
155+
def has(*args: Any, **kwargs: Any) -> bool: # type: ignore[misc] # noqa: ARG001
156+
"""Placeholder implementation"""
157+
return False
158+
159+
ATTRS_INSTALLED = False # pyright: ignore[reportConstantRedefinition]
160+
161+
try:
162+
from cattrs import structure, unstructure # pyright: ignore # type: ignore[import-not-found]
163+
164+
CATTRS_INSTALLED = True
165+
except ImportError:
166+
167+
def unstructure(*args: Any, **kwargs: Any) -> Any: # noqa: ARG001
168+
"""Placeholder implementation"""
169+
return {}
170+
171+
def structure(*args: Any, **kwargs: Any) -> Any: # noqa: ARG001
172+
"""Placeholder implementation"""
173+
return {}
174+
175+
CATTRS_INSTALLED = False # pyright: ignore[reportConstantRedefinition]
176+
129177
__all__ = (
178+
"ATTRS_INSTALLED",
179+
"CATTRS_INSTALLED",
130180
"LITESTAR_INSTALLED",
131181
"MSGSPEC_INSTALLED",
132182
"PYDANTIC_INSTALLED",
133183
"UNSET",
184+
"AttrsInstance",
134185
"BaseModel",
135186
"DTOData",
136187
"FailFast",
137188
"Struct",
138189
"TypeAdapter",
139190
"UnsetType",
191+
"asdict",
140192
"convert",
193+
"define",
194+
"field",
195+
"fields",
196+
"has",
197+
"structure",
198+
"unstructure",
141199
)

0 commit comments

Comments
 (0)