--- /dev/null
+.. change::
+ :tags: bug, orm
+ :tickets: 11814
+
+ Improvements to the ORM annotated declarative type map lookup dealing with
+ composed types such as ``dict[str, Any]`` linking to JSON (or others) with
+ or without "future annotations" mode.
+
+
class _CollectedAnnotation(NamedTuple):
raw_annotation: _AnnotationScanType
mapped_container: Optional[Type[Mapped[Any]]]
- extracted_mapped_annotation: Union[Type[Any], str]
+ extracted_mapped_annotation: Union[_AnnotationScanType, str]
is_dataclass: bool
attr_value: Any
originating_module: str
de_stringify_union_elements as _de_stringify_union_elements,
)
from ..util.typing import eval_name_only as _eval_name_only
+from ..util.typing import fixup_container_fwd_refs
from ..util.typing import is_origin_of_cls
from ..util.typing import Literal
from ..util.typing import TupleAny
is_dataclass_field: bool,
expect_mapped: bool = True,
raiseerr: bool = True,
-) -> Optional[Tuple[Union[type, str], Optional[type]]]:
+) -> Optional[Tuple[Union[_AnnotationScanType, str], Optional[type]]]:
"""given an annotation, figure out if it's ``Mapped[something]`` and if
so, return the ``something`` part.
"Expected sub-type for Mapped[] annotation"
)
- return annotated.__args__[0], annotated.__origin__
+ return (
+ # fix dict/list/set args to be ForwardRef, see #11814
+ fixup_container_fwd_refs(annotated.__args__[0]),
+ annotated.__origin__,
+ )
def _mapper_property_as_plain_name(prop: Type[Any]) -> str:
)
return _copy_generic_annotation_with(annotation, elements)
+
return annotation # type: ignore
+def fixup_container_fwd_refs(
+ type_: _AnnotationScanType,
+) -> _AnnotationScanType:
+ """Correct dict['x', 'y'] into dict[ForwardRef('x'), ForwardRef('y')]
+ and similar for list, set
+
+ """
+ if (
+ is_generic(type_)
+ and type_.__origin__
+ in (
+ dict,
+ set,
+ list,
+ collections_abc.MutableSet,
+ collections_abc.MutableMapping,
+ collections_abc.MutableSequence,
+ collections_abc.Mapping,
+ collections_abc.Sequence,
+ )
+ # fight, kick and scream to struggle to tell the difference between
+ # dict[] and typing.Dict[] which DO NOT compare the same and DO NOT
+ # behave the same yet there is NO WAY to distinguish between which type
+ # it is using public attributes
+ and not re.match(
+ "typing.(?:Dict|List|Set|.*Mapping|.*Sequence|.*Set)", repr(type_)
+ )
+ ):
+ # compat with py3.10 and earlier
+ return type_.__origin__.__class_getitem__( # type: ignore
+ tuple(
+ [
+ ForwardRef(elem) if isinstance(elem, str) else elem
+ for elem in type_.__args__
+ ]
+ )
+ )
+ return type_
+
+
def _copy_generic_annotation_with(
annotation: GenericProtocol[_T], elements: Tuple[_AnnotationScanType, ...]
) -> Type[_T]:
(str, str),
),
id_="sa",
+ argnames="container_typ,args",
)
- def test_extract_generic_from_pep593(self, container_typ, args):
- """test #9099"""
+ @testing.variation("style", ["pep593", "alias", "direct"])
+ def test_extract_composed(self, container_typ, args, style):
+ """test #9099 (pep593)
+
+ test #11814
+
+ """
global TestType
- TestType = Annotated[container_typ[args], 0]
+
+ if style.pep593:
+ TestType = Annotated[container_typ[args], 0]
+ elif style.alias:
+ TestType = container_typ[args]
+ elif style.direct:
+ TestType = container_typ
+ double_strings = args == (str, str)
class Base(DeclarativeBase):
- type_annotation_map = {TestType: JSON()}
+ if style.direct:
+ if double_strings:
+ type_annotation_map = {TestType[str, str]: JSON()}
+ else:
+ type_annotation_map = {TestType[str]: JSON()}
+ else:
+ type_annotation_map = {TestType: JSON()}
class MyClass(Base):
__tablename__ = "my_table"
id: Mapped[int] = mapped_column(primary_key=True)
- data: Mapped[TestType] = mapped_column()
+
+ if style.direct:
+ if double_strings:
+ data: Mapped[TestType[str, str]] = mapped_column()
+ else:
+ data: Mapped[TestType[str]] = mapped_column()
+ else:
+ data: Mapped[TestType] = mapped_column()
is_(MyClass.__table__.c.data.type._type_affinity, JSON)
(str, str),
),
id_="sa",
+ argnames="container_typ,args",
)
- def test_extract_generic_from_pep593(self, container_typ, args):
- """test #9099"""
+ @testing.variation("style", ["pep593", "alias", "direct"])
+ def test_extract_composed(self, container_typ, args, style):
+ """test #9099 (pep593)
+
+ test #11814
+
+ """
global TestType
- TestType = Annotated[container_typ[args], 0]
+
+ if style.pep593:
+ TestType = Annotated[container_typ[args], 0]
+ elif style.alias:
+ TestType = container_typ[args]
+ elif style.direct:
+ TestType = container_typ
+ double_strings = args == (str, str)
class Base(DeclarativeBase):
- type_annotation_map = {TestType: JSON()}
+ if style.direct:
+ if double_strings:
+ type_annotation_map = {TestType[str, str]: JSON()}
+ else:
+ type_annotation_map = {TestType[str]: JSON()}
+ else:
+ type_annotation_map = {TestType: JSON()}
class MyClass(Base):
__tablename__ = "my_table"
id: Mapped[int] = mapped_column(primary_key=True)
- data: Mapped[TestType] = mapped_column()
+
+ if style.direct:
+ if double_strings:
+ data: Mapped[TestType[str, str]] = mapped_column()
+ else:
+ data: Mapped[TestType[str]] = mapped_column()
+ else:
+ data: Mapped[TestType] = mapped_column()
is_(MyClass.__table__.c.data.type._type_affinity, JSON)