]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
Fix is_pep695 misidentifying Annotated[TypeAliasType] as PEP 695
authorMike Bayer <mike_mp@zzzcomputing.com>
Wed, 17 Jun 2026 22:06:56 +0000 (18:06 -0400)
committerMike Bayer <mike_mp@zzzcomputing.com>
Wed, 17 Jun 2026 22:06:56 +0000 (18:06 -0400)
The is_pep695() function incorrectly identified
Annotated[TypeAliasType, ...] as a PEP 695 type alias because
Annotated's __origin__ attribute returns the first type argument
(the TypeAliasType) rather than Annotated itself.  This caused
_init_column_for_annotation to crash with AttributeError when
attempting to access __value__ on the Annotated wrapper.

Added a check for is_pep593() before recursing through __origin__
in is_pep695(), so Annotated types are correctly excluded.

Fixes: #13386
Change-Id: I36ef83ebbab5abc08bed0131efb552c3fc001911

doc/build/changelog/unreleased_20/13386.rst [new file with mode: 0644]
lib/sqlalchemy/util/typing.py
test/base/test_typing_utils.py
test/orm/declarative/test_tm_future_annotations_sync.py
test/orm/declarative/test_typed_mapping.py

diff --git a/doc/build/changelog/unreleased_20/13386.rst b/doc/build/changelog/unreleased_20/13386.rst
new file mode 100644 (file)
index 0000000..548b439
--- /dev/null
@@ -0,0 +1,10 @@
+.. change::
+  :tags: bug, orm declarative
+  :tickets: 13386
+
+  Fixed issue where using :pep:`593` ``Annotated`` wrapping a :pep:`695`
+  ``type`` alias, such as ``Annotated[SomeTypeAlias, mapped_column()]``,
+  would crash with ``AttributeError: __value__``. The internal
+  ``is_pep695()`` check incorrectly identified the ``Annotated`` type as a
+  PEP 695 type alias due to a quirk in ``Annotated.__origin__`` returning
+  the first type argument rather than ``Annotated`` itself.
index ef5b171b0a4c716fc445bc581ad434fe2ff6ccdd..4c7a4ba58c8c24bf45e095983e365787f56334de 100644 (file)
@@ -344,6 +344,8 @@ def is_pep695(type_: _AnnotationScanType) -> TypeGuard[TypeAliasType]:
     # though.
     # NOTE: things seems to work also without this additional check
     if is_generic(type_):
+        if is_pep593(type_):
+            return False
         return is_pep695(type_.__origin__)
     return isinstance(type_, _type_instances.TypeAliasType)
 
index e66431066fb6bff22472a52dee681d0aaa0c2165..a760a650319bff90e246b017be0a18415da4badd 100644 (file)
@@ -196,6 +196,8 @@ A_union = typing.Annotated[typing.Union[str, int], "other_meta"]
 A_null_union = typing.Annotated[
     typing.Union[str, int, None], "other_meta", "null"
 ]
+A_pep695 = typing.Annotated[TA_int, "meta"]
+A_pep695_ext = typing.Annotated[TAext_int, "meta"]
 
 
 def compare_type_by_string(a, b):
@@ -357,6 +359,12 @@ class TestTyping(fixtures.TestBase):
         for t in type_aliases():
             eq_(sa_typing.is_pep695(t), True)
 
+    def test_is_pep695_annotated_pep695(self):
+        """test #13386"""
+        for t in (A_pep695, A_pep695_ext):
+            eq_(sa_typing.is_pep695(t), False)
+            eq_(sa_typing.is_pep593(t), True)
+
     def test_pep695_value(self):
         eq_(sa_typing.pep695_values(int), {int})
         eq_(
index 2c027642b74aa1984fb33538f596330968d9fcb9..d0bed9789a8db097f9c9fce4d096b9d91a7e375c 100644 (file)
@@ -160,6 +160,7 @@ def pep_695_types():
 def pep_593_types(pep_695_types):
     global _GenericPep593TypeAlias, _GenericPep593Pep695
     global _RecursivePep695Pep593
+    global _AnnotatedPep695
 
     _GenericPep593TypeAlias = Annotated[
         TV, mapped_column(info={"hi": "there"})  # type: ignore
@@ -179,6 +180,11 @@ def pep_593_types(pep_695_types):
         ],
     )
 
+    _AnnotatedPep695 = Annotated[
+        _TypingStrPep695,  # type: ignore
+        mapped_column(JSON),
+    ]
+
 
 def expect_annotation_syntax_error(name):
     return expect_raises_message(
@@ -1318,6 +1324,23 @@ class Pep593InterpretationTests(fixtures.TestBase, testing.AssertsCompiledSQL):
         ):
             declare()
 
+    @testing.requires.python312
+    def test_pep593_wrapping_pep695(
+        self, decl_base: Type[DeclarativeBase], pep_593_types
+    ):
+        """test #13386"""
+
+        class MyClass(decl_base):
+            __tablename__ = "my_table"
+
+            id: Mapped[int] = mapped_column(primary_key=True)
+
+            data_one: Mapped[_AnnotatedPep695]  # noqa: F821
+
+        table = MyClass.__table__
+        assert table is not None
+        is_(MyClass.data_one.expression.type.__class__, JSON)
+
     def test_extract_base_type_from_pep593(
         self, decl_base: Type[DeclarativeBase]
     ):
index 0d2c6cdc33ebc80d395e0846a3d6eaab67b14b25..af6c92ab1e4c0cb4f66820346cb649f1cce8359c 100644 (file)
@@ -151,6 +151,7 @@ def pep_695_types():
 def pep_593_types(pep_695_types):
     global _GenericPep593TypeAlias, _GenericPep593Pep695
     global _RecursivePep695Pep593
+    global _AnnotatedPep695
 
     _GenericPep593TypeAlias = Annotated[
         TV, mapped_column(info={"hi": "there"})  # type: ignore
@@ -170,6 +171,11 @@ def pep_593_types(pep_695_types):
         ],
     )
 
+    _AnnotatedPep695 = Annotated[
+        _TypingStrPep695,  # type: ignore
+        mapped_column(JSON),
+    ]
+
 
 def expect_annotation_syntax_error(name):
     return expect_raises_message(
@@ -1309,6 +1315,23 @@ class Pep593InterpretationTests(fixtures.TestBase, testing.AssertsCompiledSQL):
         ):
             declare()
 
+    @testing.requires.python312
+    def test_pep593_wrapping_pep695(
+        self, decl_base: Type[DeclarativeBase], pep_593_types
+    ):
+        """test #13386"""
+
+        class MyClass(decl_base):
+            __tablename__ = "my_table"
+
+            id: Mapped[int] = mapped_column(primary_key=True)
+
+            data_one: Mapped[_AnnotatedPep695]  # noqa: F821
+
+        table = MyClass.__table__
+        assert table is not None
+        is_(MyClass.data_one.expression.type.__class__, JSON)
+
     def test_extract_base_type_from_pep593(
         self, decl_base: Type[DeclarativeBase]
     ):