From: Mike Bayer Date: Wed, 17 Jun 2026 22:06:56 +0000 (-0400) Subject: Fix is_pep695 misidentifying Annotated[TypeAliasType] as PEP 695 X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=7a823b5f81c05407513efb84d13b67731635154d;p=thirdparty%2Fsqlalchemy%2Fsqlalchemy.git Fix is_pep695 misidentifying Annotated[TypeAliasType] as PEP 695 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 --- diff --git a/doc/build/changelog/unreleased_20/13386.rst b/doc/build/changelog/unreleased_20/13386.rst new file mode 100644 index 0000000000..548b439798 --- /dev/null +++ b/doc/build/changelog/unreleased_20/13386.rst @@ -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. diff --git a/lib/sqlalchemy/util/typing.py b/lib/sqlalchemy/util/typing.py index ef5b171b0a..4c7a4ba58c 100644 --- a/lib/sqlalchemy/util/typing.py +++ b/lib/sqlalchemy/util/typing.py @@ -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) diff --git a/test/base/test_typing_utils.py b/test/base/test_typing_utils.py index e66431066f..a760a65031 100644 --- a/test/base/test_typing_utils.py +++ b/test/base/test_typing_utils.py @@ -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_( diff --git a/test/orm/declarative/test_tm_future_annotations_sync.py b/test/orm/declarative/test_tm_future_annotations_sync.py index 2c027642b7..d0bed9789a 100644 --- a/test/orm/declarative/test_tm_future_annotations_sync.py +++ b/test/orm/declarative/test_tm_future_annotations_sync.py @@ -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] ): diff --git a/test/orm/declarative/test_typed_mapping.py b/test/orm/declarative/test_typed_mapping.py index 0d2c6cdc33..af6c92ab1e 100644 --- a/test/orm/declarative/test_typed_mapping.py +++ b/test/orm/declarative/test_typed_mapping.py @@ -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] ):