Skip to content

Commit af64f7e

Browse files
authored
fix: correctly set uniquify from new (#424)
- Correctly sets `uniquify` when passed in from `new` or `__init__` - Introduced tests for `sync_tools` utilities, including `maybe_async_`, `maybe_async_context`, `SoonValue`, `TaskGroup`, and others. - Ensured comprehensive coverage for async and sync function handling, context managers, and value management. - Improved overall test suite reliability and maintainability.
1 parent 2bef7ed commit af64f7e

21 files changed

Lines changed: 796 additions & 282 deletions

File tree

advanced_alchemy/_serialization.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from typing_extensions import runtime_checkable
66

77
try:
8-
from pydantic import BaseModel # type: ignore # noqa: PGH003
8+
from pydantic import BaseModel # type: ignore
99

1010
PYDANTIC_INSTALLED = True
1111
except ImportError:
@@ -18,7 +18,11 @@ class BaseModel(Protocol): # type: ignore[no-redef]
1818
model_fields: ClassVar[dict[str, Any]]
1919

2020
def model_dump_json(self, *args: Any, **kwargs: Any) -> str:
21-
"""Placeholder"""
21+
"""Placeholder for pydantic.BaseModel.model_dump_json
22+
23+
Returns:
24+
The JSON representation of the model.
25+
"""
2226
return ""
2327

2428
PYDANTIC_INSTALLED = False # pyright: ignore[reportConstantRedefinition]

advanced_alchemy/alembic/templates/asyncio/env.py

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020

2121
# this is the Alembic Config object, which provides
2222
# access to the values within the .ini file in use.
23-
config: "AlembicCommandConfig" = context.config # type: ignore # noqa: PGH003
23+
config: "AlembicCommandConfig" = context.config # type: ignore
2424
writer = rewriter.Rewriter()
2525

2626

@@ -30,7 +30,16 @@ def order_columns(
3030
revision: tuple[str, ...], # noqa: ARG001
3131
op: ops.CreateTableOp,
3232
) -> ops.CreateTableOp:
33-
"""Orders ID first and the audit columns at the end."""
33+
"""Orders ID first and the audit columns at the end.
34+
35+
Args:
36+
context: The context of the environment.
37+
revision: The revision of the environment.
38+
op: The operation to create the table.
39+
40+
Returns:
41+
The operation to create the table.
42+
"""
3443
special_names = {"id": -100, "sa_orm_sentinel": 3001, "created_at": 3002, "updated_at": 3003}
3544
cols_by_key = [ # pyright: ignore[reportUnknownVariableType]
3645
(
@@ -100,6 +109,9 @@ async def run_migrations_online() -> None:
100109
101110
In this scenario we need to create an Engine and associate a
102111
connection with the context.
112+
113+
Raises:
114+
RuntimeError: If the engine cannot be created from the config.
103115
"""
104116
configuration = config.get_section(config.config_ini_section) or {}
105117
configuration["sqlalchemy.url"] = config.db_url

advanced_alchemy/alembic/templates/sync/env.py

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import operator
12
from typing import TYPE_CHECKING, cast
23

34
from sqlalchemy import Column, Engine, engine_from_config, pool
@@ -18,7 +19,7 @@
1819

1920
# this is the Alembic Config object, which provides
2021
# access to the values within the .ini file in use.
21-
config: "AlembicCommandConfig" = context.config # type: ignore # noqa: PGH003
22+
config: "AlembicCommandConfig" = context.config # type: ignore
2223
writer = rewriter.Rewriter()
2324

2425

@@ -28,7 +29,16 @@ def order_columns(
2829
revision: tuple[str, ...], # noqa: ARG001
2930
op: ops.CreateTableOp,
3031
) -> ops.CreateTableOp:
31-
"""Orders ID first and the audit columns at the end."""
32+
"""Orders ID first and the audit columns at the end.
33+
34+
Args:
35+
context: The context of the environment.
36+
revision: The revision of the environment.
37+
op: The operation to create the table.
38+
39+
Returns:
40+
The operation to create the table.
41+
"""
3242
special_names = {"id": -100, "sa_orm_sentinel": 3001, "created_at": 3002, "updated_at": 3003}
3343
cols_by_key = [ # pyright: ignore[reportUnknownVariableType]
3444
(
@@ -37,7 +47,7 @@ def order_columns(
3747
)
3848
for index, col in enumerate(op.columns)
3949
]
40-
columns = [col for _, col in sorted(cols_by_key, key=lambda entry: entry[0])] # pyright: ignore[reportUnknownVariableType,reportUnknownArgumentType,reportUnknownLambdaType]
50+
columns = [col for _, col in sorted(cols_by_key, key=operator.itemgetter(0))] # pyright: ignore[reportUnknownVariableType,reportUnknownArgumentType,reportUnknownLambdaType]
4151
return ops.CreateTableOp(
4252
op.table_name,
4353
columns, # pyright: ignore[reportUnknownArgumentType]
@@ -97,6 +107,9 @@ def run_migrations_online() -> None:
97107
98108
In this scenario we need to create an Engine and associate a
99109
connection with the context.
110+
111+
Raises:
112+
RuntimeError: If the engine cannot be created from the config.
100113
"""
101114
configuration = config.get_section(config.config_ini_section) or {}
102115
configuration["sqlalchemy.url"] = config.db_url

advanced_alchemy/config/common.py

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ class GenericSessionConfig(Generic[ConnectionT, EngineT, SessionT]):
8080
autoflush: "Union[bool, EmptyType]" = Empty
8181
"""When ``True``, all query operations will issue a flush call to this :class:`Session <sqlalchemy.orm.Session>`
8282
before proceeding"""
83-
bind: "Union[EngineT, ConnectionT, None, EmptyType]" = Empty
83+
bind: "Optional[Union[EngineT, ConnectionT, EmptyType]]" = Empty
8484
"""The :class:`Engine <sqlalchemy.engine.Engine>` or :class:`Connection <sqlalchemy.engine.Connection>` that new
8585
:class:`Session <sqlalchemy.orm.Session>` objects will be bound to."""
8686
binds: "Union[dict[Union[type[Any], Mapper[Any], TableClause, str], Union[EngineT, ConnectionT]], None, EmptyType]" = Empty
@@ -234,6 +234,9 @@ def session_config_dict(self) -> dict[str, Any]:
234234
def get_engine(self) -> EngineT:
235235
"""Return an engine. If none exists yet, create one.
236236
237+
Raises:
238+
ImproperConfigurationError: if neither `connection_string` nor `engine_instance` are provided.
239+
237240
Returns:
238241
:class:`sqlalchemy.Engine` or :class:`sqlalchemy.ext.asyncio.AsyncEngine` instance used by the plugin.
239242
"""
@@ -246,12 +249,13 @@ def get_engine(self) -> EngineT:
246249

247250
engine_config = self.engine_config_dict
248251
try:
249-
return self.create_engine_callable(self.connection_string, **engine_config)
252+
self.engine_instance = self.create_engine_callable(self.connection_string, **engine_config)
250253
except TypeError:
251254
# likely due to a dialect that doesn't support json type
252255
del engine_config["json_deserializer"]
253256
del engine_config["json_serializer"]
254-
return self.create_engine_callable(self.connection_string, **engine_config)
257+
self.engine_instance = self.create_engine_callable(self.connection_string, **engine_config)
258+
return self.engine_instance
255259

256260
def create_session_maker(self) -> "Callable[[], SessionT]": # pragma: no cover
257261
"""Get a session maker. If none exists yet, create one.
@@ -265,7 +269,8 @@ def create_session_maker(self) -> "Callable[[], SessionT]": # pragma: no cover
265269
session_kws = self.session_config_dict
266270
if session_kws.get("bind") is None:
267271
session_kws["bind"] = self.get_engine()
268-
return cast("Callable[[], SessionT]", self.session_maker_class(**session_kws))
272+
self.session_maker = cast("Callable[[], SessionT]", self.session_maker_class(**session_kws))
273+
return self.session_maker
269274

270275

271276
@dataclass

advanced_alchemy/extensions/litestar/dto.py

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -294,8 +294,6 @@ def generate_field_definitions(cls, model_type: type[DeclarativeBase]) -> Genera
294294
295295
Raises:
296296
RuntimeError: If the mapper cannot be found for the model type.
297-
NotImplementedError: If an unsupported property or extension type is encountered.
298-
ImproperConfigurationError: If a type cannot be parsed from an element.
299297
"""
300298
if (mapper := inspect(model_type)) is None: # pragma: no cover # pyright: ignore[reportUnnecessaryComparison]
301299
msg = "Unexpected `None` value for mapper." # type: ignore[unreachable]
@@ -330,7 +328,7 @@ def generate_field_definitions(cls, model_type: type[DeclarativeBase]) -> Genera
330328
should_skip_descriptor = False
331329
dto_field: Optional[DTOField] = None
332330
if hasattr(orm_descriptor, "property"): # pyright: ignore[reportUnknownArgumentType]
333-
dto_field = orm_descriptor.property.info.get(DTO_FIELD_META_KEY) # pyright: ignore # noqa: PGH003
331+
dto_field = orm_descriptor.property.info.get(DTO_FIELD_META_KEY) # pyright: ignore
334332

335333
# Case 1
336334
is_field_marked_not_private = dto_field and dto_field.mark is not Mark.PRIVATE # pyright: ignore[reportUnknownVariableType,reportUnknownMemberType]
@@ -370,7 +368,7 @@ def detect_nested_field(cls, field_definition: FieldDefinition) -> bool:
370368

371369
def _detect_defaults(elem: ElementType) -> tuple[Any, Any]:
372370
default: Any = Empty
373-
default_factory: Any = None # pyright:ignore # noqa: PGH003
371+
default_factory: Any = None # pyright:ignore
374372
if sqla_default := getattr(elem, "default", None):
375373
if sqla_default.is_scalar:
376374
default = sqla_default.arg
@@ -402,11 +400,11 @@ def parse_type_from_element(elem: ElementType, orm_descriptor: InspectionAttr) -
402400
elem: The SQLAlchemy element to parse.
403401
orm_descriptor: The attribute `elem` was extracted from.
404402
403+
Raises:
404+
ImproperConfigurationError: If the type cannot be parsed.
405+
405406
Returns:
406407
FieldDefinition: The parsed type.
407-
408-
Raises:
409-
ImproperlyConfiguredException: If the type cannot be parsed.
410408
"""
411409

412410
if isinstance(elem, Column):
@@ -415,7 +413,7 @@ def parse_type_from_element(elem: ElementType, orm_descriptor: InspectionAttr) -
415413
return FieldDefinition.from_annotation(elem.type.python_type)
416414

417415
if isinstance(elem, RelationshipProperty):
418-
if elem.direction in (RelationshipDirection.ONETOMANY, RelationshipDirection.MANYTOMANY):
416+
if elem.direction in {RelationshipDirection.ONETOMANY, RelationshipDirection.MANYTOMANY}:
419417
collection_type = FieldDefinition.from_annotation(elem.collection_class or list) # pyright: ignore[reportUnknownMemberType]
420418
return FieldDefinition.from_annotation(collection_type.safe_generic_origin[elem.mapper.class_])
421419

@@ -431,9 +429,7 @@ def parse_type_from_element(elem: ElementType, orm_descriptor: InspectionAttr) -
431429
return FieldDefinition.from_annotation(orm_descriptor.type.python_type)
432430

433431
msg = f"Unable to parse type from element '{elem}'. Consider adding a type hint."
434-
raise ImproperConfigurationError(
435-
msg,
436-
)
432+
raise ImproperConfigurationError(msg)
437433

438434

439435
def detect_nullable_relationship(elem: RelationshipProperty[Any]) -> bool:

advanced_alchemy/extensions/litestar/providers.py

Lines changed: 6 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
SQLAlchemyAsyncRepositoryService,
4444
SQLAlchemySyncRepositoryService,
4545
)
46+
from advanced_alchemy.utils.singleton import SingletonMeta
4647

4748
if TYPE_CHECKING:
4849
from sqlalchemy import Select
@@ -109,17 +110,6 @@ class FilterConfig(TypedDict):
109110
"""When set, updated_at filter is enabled."""
110111

111112

112-
class SingletonMeta(type):
113-
"""Metaclass for singleton pattern."""
114-
115-
_instances: dict[type, Any] = {}
116-
117-
def __call__(cls, *args: Any, **kwargs: Any) -> Any:
118-
if cls not in cls._instances: # pyright: ignore[reportUnnecessaryContains]
119-
cls._instances[cls] = super().__call__(*args, **kwargs)
120-
return cls._instances[cls]
121-
122-
123113
class DependencyCache(metaclass=SingletonMeta):
124114
"""Simple dependency cache for the application. This is used to help memoize dependencies that are generated dynamically."""
125115

@@ -175,7 +165,11 @@ def create_service_provider(
175165
uniquify: Optional[bool] = None,
176166
count_with_window_function: Optional[bool] = None,
177167
) -> Callable[..., Union["AsyncGenerator[AsyncServiceT_co, None]", "Generator[SyncServiceT_co,None, None]"]]:
178-
"""Create a dependency provider for a service."""
168+
"""Create a dependency provider for a service.
169+
170+
Returns:
171+
A dependency provider for the service.
172+
"""
179173
if issubclass(service_class, SQLAlchemyAsyncRepositoryService) or service_class is SQLAlchemyAsyncRepositoryService: # type: ignore[comparison-overlap]
180174

181175
async def provide_async_service(

advanced_alchemy/extensions/sanic/config.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
SANIC_INSTALLED = True
2222
except ModuleNotFoundError: # pragma: no cover
2323
SANIC_INSTALLED = False # pyright: ignore[reportConstantRedefinition]
24-
Extend = type("Extend", (), {}) # type: ignore # noqa: PGH003
24+
Extend = type("Extend", (), {}) # type: ignore
2525

2626
from sqlalchemy.ext.asyncio import AsyncEngine, AsyncSession, async_sessionmaker
2727
from sqlalchemy.orm import Session, sessionmaker

advanced_alchemy/extensions/sanic/extension.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@
1414
SANIC_INSTALLED = True
1515
except ModuleNotFoundError: # pragma: no cover
1616
SANIC_INSTALLED = False # pyright: ignore[reportConstantRedefinition]
17-
Extension = type("Extension", (), {}) # type: ignore # noqa: PGH003
18-
Extend = type("Extend", (), {}) # type: ignore # noqa: PGH003
17+
Extension = type("Extension", (), {}) # type: ignore
18+
Extend = type("Extend", (), {}) # type: ignore
1919

2020
if TYPE_CHECKING:
2121
from sanic import Sanic

0 commit comments

Comments
 (0)