From 046272e06aa3284a87e0dd1f90d2242fb434de10 Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Mon, 16 Jan 2023 10:31:39 -0500 Subject: dont assume copy_with() on builtins list, dict, etc; improve error msg. Fixed issue where using an ``Annotated`` type in the ``type_annotation_map`` which itself contained a plain container type (e.g. ``list``, ``dict``) generic type as the target type would produce an internal error where the ORM were trying to interpret the ``Annotated`` instance. Added an error message when a :func:`_orm.relationship` is mapped against an abstract container type, such as ``Mapped[Sequence[B]]``, without providing the :paramref:`_orm.relationship.container_class` parameter which is necessary when the type is abstract. Previously the the abstract container would attempt to be instantiated and fail. Fixes: #9099 Fixes: #9100 Change-Id: I18aa6abd5451c5ac75a9caed8441ff0cd8f44589 --- lib/sqlalchemy/orm/relationships.py | 12 ++++++++++++ lib/sqlalchemy/util/typing.py | 21 ++++++++++++++++++--- 2 files changed, 30 insertions(+), 3 deletions(-) (limited to 'lib') diff --git a/lib/sqlalchemy/orm/relationships.py b/lib/sqlalchemy/orm/relationships.py index bfd39c369..66d3a6035 100644 --- a/lib/sqlalchemy/orm/relationships.py +++ b/lib/sqlalchemy/orm/relationships.py @@ -18,6 +18,7 @@ from __future__ import annotations import collections from collections import abc import dataclasses +import inspect as _py_inspect import re import typing from typing import Any @@ -1768,7 +1769,18 @@ class RelationshipProperty( arg_origin, abc.Collection ): if self.collection_class is None: + if _py_inspect.isabstract(arg_origin): + raise sa_exc.ArgumentError( + f"Collection annotation type {arg_origin} cannot " + "be instantiated; please provide an explicit " + "'collection_class' parameter " + "(e.g. list, set, etc.) to the " + "relationship() function to accompany this " + "annotation" + ) + self.collection_class = arg_origin + elif not is_write_only and not is_dynamic: self.uselist = False diff --git a/lib/sqlalchemy/util/typing.py b/lib/sqlalchemy/util/typing.py index b1ef87db1..e1670ed21 100644 --- a/lib/sqlalchemy/util/typing.py +++ b/lib/sqlalchemy/util/typing.py @@ -97,8 +97,12 @@ class GenericProtocol(Protocol[_T]): __args__: Tuple[_AnnotationScanType, ...] __origin__: Type[_T] - def copy_with(self, params: Tuple[_AnnotationScanType, ...]) -> Type[_T]: - ... + # Python's builtin _GenericAlias has this method, however builtins like + # list, dict, etc. do not, even though they have ``__origin__`` and + # ``__args__`` + # + # def copy_with(self, params: Tuple[_AnnotationScanType, ...]) -> Type[_T]: + # ... class SupportsKeysAndGetItem(Protocol[_KT, _VT_co]): @@ -158,10 +162,21 @@ def de_stringify_annotation( for elem in annotation.__args__ ) - return annotation.copy_with(elements) + return _copy_generic_annotation_with(annotation, elements) return annotation # type: ignore +def _copy_generic_annotation_with( + annotation: GenericProtocol[_T], elements: Tuple[_AnnotationScanType, ...] +) -> Type[_T]: + if hasattr(annotation, "copy_with"): + # List, Dict, etc. real generics + return annotation.copy_with(elements) # type: ignore + else: + # Python builtins list, dict, etc. + return annotation.__origin__[elements] # type: ignore + + def eval_expression(expression: str, module_name: str) -> Any: try: base_globals: Dict[str, Any] = sys.modules[module_name].__dict__ -- cgit v1.2.1