1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252
|
"""Tests for cattrs integration with attrs support in Advanced Alchemy services."""
from __future__ import annotations
import unittest.mock as mock
from typing import Optional
import pytest
from advanced_alchemy.service.typing import (
ATTRS_INSTALLED,
CATTRS_INSTALLED,
schema_dump,
)
# pyright: reportAttributeAccessIssue=false
pytestmark = [
pytest.mark.unit,
pytest.mark.skipif(not ATTRS_INSTALLED, reason="attrs not installed"),
]
if ATTRS_INSTALLED:
from attrs import define
@define
class SimpleAttrsModel:
"""Simple attrs model for testing."""
name: str
age: int
email: Optional[str] = None
class TestCattrsIntegration:
"""Test cattrs integration scenarios."""
@pytest.mark.skipif(not CATTRS_INSTALLED, reason="cattrs not installed")
def test_schema_dump_with_cattrs_enabled(self) -> None:
"""Test schema_dump uses cattrs when available."""
instance = SimpleAttrsModel(name="John", age=30, email="john@example.com")
result = schema_dump(instance)
assert isinstance(result, dict)
assert result["name"] == "John"
assert result["age"] == 30
assert result["email"] == "john@example.com"
def test_schema_dump_with_cattrs_disabled(self) -> None:
"""Test schema_dump falls back to attrs.asdict when cattrs is disabled."""
from advanced_alchemy.service import typing as service_typing
instance = SimpleAttrsModel(name="Jane", age=25)
# Mock CATTRS_INSTALLED to be False
with mock.patch.object(service_typing, "CATTRS_INSTALLED", False):
result = schema_dump(instance)
assert isinstance(result, dict)
assert result["name"] == "Jane"
assert result["age"] == 25
assert result["email"] is None
@pytest.mark.skipif(not CATTRS_INSTALLED, reason="cattrs not installed")
def test_to_schema_with_cattrs_priority(self) -> None:
"""Test that to_schema uses cattrs over attrs when both are available."""
from advanced_alchemy.service._util import ResultConverter
converter = ResultConverter()
data = {"name": "Alice", "age": 28, "email": "alice@example.com"}
result = converter.to_schema(data, schema_type=SimpleAttrsModel)
assert isinstance(result, SimpleAttrsModel)
assert result.name == "Alice"
assert result.age == 28
assert result.email == "alice@example.com"
def test_to_schema_attrs_fallback_when_cattrs_disabled(self) -> None:
"""Test that to_schema falls back to attrs when cattrs is disabled."""
from advanced_alchemy.service import _util
from advanced_alchemy.service import typing as service_typing
from advanced_alchemy.service._util import ResultConverter
converter = ResultConverter()
data = {"name": "Bob", "age": 35, "email": "bob@example.com"}
# Mock CATTRS_INSTALLED to be False in both modules
with (
mock.patch.object(service_typing, "CATTRS_INSTALLED", False),
mock.patch.object(_util, "CATTRS_INSTALLED", False),
):
result = converter.to_schema(data, schema_type=SimpleAttrsModel)
assert isinstance(result, SimpleAttrsModel)
assert result.name == "Bob"
assert result.age == 35
assert result.email == "bob@example.com"
def test_to_schema_sequence_with_cattrs_disabled(self) -> None:
"""Test that to_schema handles sequences correctly when cattrs is disabled."""
from advanced_alchemy.service import _util
from advanced_alchemy.service import typing as service_typing
from advanced_alchemy.service._util import ResultConverter
from advanced_alchemy.service.pagination import OffsetPagination
converter = ResultConverter()
data = [
{"name": "Charlie", "age": 40},
{"name": "Diana", "age": 45},
]
# Mock CATTRS_INSTALLED to be False in both modules
with (
mock.patch.object(service_typing, "CATTRS_INSTALLED", False),
mock.patch.object(_util, "CATTRS_INSTALLED", False),
):
result = converter.to_schema(data, schema_type=SimpleAttrsModel)
assert isinstance(result, OffsetPagination)
assert len(result.items) == 2
assert all(isinstance(item, SimpleAttrsModel) for item in result.items)
assert result.items[0].name == "Charlie"
assert result.items[1].name == "Diana"
@pytest.mark.skipif(not CATTRS_INSTALLED, reason="cattrs not installed")
def test_cattrs_structure_direct_usage(self) -> None:
"""Test direct usage of cattrs structure function."""
from advanced_alchemy.service.typing import structure
data = {"name": "Eve", "age": 33, "email": "eve@example.com"}
result = structure(data, SimpleAttrsModel)
assert isinstance(result, SimpleAttrsModel)
assert result.name == "Eve"
assert result.age == 33
assert result.email == "eve@example.com"
@pytest.mark.skipif(not CATTRS_INSTALLED, reason="cattrs not installed")
def test_cattrs_unstructure_direct_usage(self) -> None:
"""Test direct usage of cattrs unstructure function."""
from advanced_alchemy.service.typing import unstructure
instance = SimpleAttrsModel(name="Frank", age=28)
result = unstructure(instance)
assert isinstance(result, dict)
assert result["name"] == "Frank"
assert result["age"] == 28
assert result["email"] is None
def test_performance_with_cached_field_names(self) -> None:
"""Test that field name caching improves performance."""
from advanced_alchemy.service import _util
from advanced_alchemy.service import typing as service_typing
from advanced_alchemy.service._util import ResultConverter, _get_attrs_field_names
converter = ResultConverter()
# Clear cache to ensure we're testing caching behavior
_get_attrs_field_names.cache_clear()
# Mock CATTRS_INSTALLED in both modules to be False to use attrs path
with (
mock.patch.object(service_typing, "CATTRS_INSTALLED", False),
mock.patch.object(_util, "CATTRS_INSTALLED", False),
):
# First call should populate the cache
data1 = {"name": "Grace", "age": 30}
result1 = converter.to_schema(data1, schema_type=SimpleAttrsModel)
# Check cache info - should have 1 miss after first call
cache_info_after_first = _get_attrs_field_names.cache_info()
# Second call should use cached field names
data2 = {"name": "Henry", "age": 35}
result2 = converter.to_schema(data2, schema_type=SimpleAttrsModel)
# Check cache info - should have 1 hit now
cache_info_after_second = _get_attrs_field_names.cache_info()
assert isinstance(result1, SimpleAttrsModel)
assert isinstance(result2, SimpleAttrsModel)
assert result1.name == "Grace"
assert result2.name == "Henry"
# Verify caching is working - should have 1 miss and 1 hit
assert cache_info_after_first.misses == 1
assert cache_info_after_second.hits >= 1
assert cache_info_after_second.currsize >= 1
def test_integration_with_both_libraries_available(self) -> None:
"""Test behavior when both cattrs and attrs are available."""
if not CATTRS_INSTALLED:
pytest.skip("cattrs not available for this test")
from advanced_alchemy.service._util import ResultConverter
converter = ResultConverter()
data = {"name": "Ivy", "age": 29, "email": "ivy@example.com"}
# Should prefer cattrs path when both are available
result = converter.to_schema(data, schema_type=SimpleAttrsModel)
assert isinstance(result, SimpleAttrsModel)
assert result.name == "Ivy"
assert result.age == 29
assert result.email == "ivy@example.com"
def test_edge_case_missing_fields(self) -> None:
"""Test handling of missing fields in data."""
from advanced_alchemy.service import _util
from advanced_alchemy.service import typing as service_typing
from advanced_alchemy.service._util import ResultConverter
converter = ResultConverter()
# Data missing the 'age' field
data = {"name": "Jack"}
# Mock CATTRS_INSTALLED to be False in both modules to test attrs path
with (
mock.patch.object(service_typing, "CATTRS_INSTALLED", False),
mock.patch.object(_util, "CATTRS_INSTALLED", False),
):
# This should handle missing fields gracefully
with pytest.raises(TypeError): # attrs will complain about missing required field
converter.to_schema(data, schema_type=SimpleAttrsModel)
def test_extra_fields_filtered_out(self) -> None:
"""Test that extra fields not in attrs schema are filtered out."""
from advanced_alchemy.service import _util
from advanced_alchemy.service import typing as service_typing
from advanced_alchemy.service._util import ResultConverter
converter = ResultConverter()
# Data with extra field 'phone' not in SimpleAttrsModel
data = {"name": "Kate", "age": 32, "email": "kate@example.com", "phone": "123-456-7890"}
# Mock CATTRS_INSTALLED to be False in both modules to test attrs filtering path
with (
mock.patch.object(service_typing, "CATTRS_INSTALLED", False),
mock.patch.object(_util, "CATTRS_INSTALLED", False),
):
result = converter.to_schema(data, schema_type=SimpleAttrsModel)
assert isinstance(result, SimpleAttrsModel)
assert result.name == "Kate"
assert result.age == 32
assert result.email == "kate@example.com"
# Extra field should not cause issues
assert not hasattr(result, "phone")
|