]> 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>
Thu, 18 Jun 2026 02:09:50 +0000 (22:09 -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 b4875b68f7a149cd4cbbd5d00ca95f406d087257..ad6dc7e637449f6b0181a6c91c5961413a5bb619 100644 (file)
@@ -357,6 +357,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 2cf95689d2224b2c39e3b9e148edd0f6405ad3ab..f3e1ab80dc8ff3b22cff41752c92bfa0406d0930 100644 (file)
@@ -202,6 +202,8 @@ A_union = typing_extensions.Annotated[typing.Union[str, int], "other_meta"]
 A_null_union = typing_extensions.Annotated[
     typing.Union[str, int, None], "other_meta", "null"
 ]
+A_pep695 = typing_extensions.Annotated[TA_int, "meta"]
+A_pep695_ext = typing_extensions.Annotated[TAext_int, "meta"]
 
 
 def compare_type_by_string(a, b):
@@ -365,6 +367,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)
+
     @requires.python38
     def test_pep695_value(self):
         eq_(sa_typing.pep695_values(int), {int})
index c5eb66b40658190c19671108ebaea5b62a8c1e01..f0ed609e09e5f97c98eb488258456f16cd84b17e 100644 (file)
@@ -161,6 +161,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
@@ -180,6 +181,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(
@@ -1389,6 +1395,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 42c11fd0fad96de74b1198b19996a8e78e8b8761..eade9d4af16cca639a6f895643397eb8248c41ca 100644 (file)
@@ -152,6 +152,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
@@ -171,6 +172,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(
@@ -1380,6 +1386,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]
     ):