]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
doc updates, localize test fixtures
authorMike Bayer <mike_mp@zzzcomputing.com>
Sun, 7 Sep 2025 00:20:00 +0000 (20:20 -0400)
committerMike Bayer <mike_mp@zzzcomputing.com>
Sun, 7 Sep 2025 00:20:00 +0000 (20:20 -0400)
testing with types is inherently awkward and subject
to changes in python interpreters (such as all the recent python 3.14
stuff we had them fix), but in this suite we already have a lot of
types that are defined inline inside of test methods.   so since that's
how many of the tests work anyway, organize the big series of pep-695
and pep-593 structures into fixtures or individual tests to make
the whole suite easier to follow.   pyright complains quite a lot
about this, so if this becomes a bigger issue for say mypy /pep484
target, we may have to revisit (which I'd likely do with more ignores)
or if function/method-local type declarations with global becomes a runtime
issue in py3.15 or something, we can revisit then where we would in
theory need to convert the entire suite, which I'd do with a more
consistent naming style for everything.

but for now try to go with fixtures / local type declarations so that
we dont have to wonder where all these types are used.

Change-Id: Ibe8f447eaa10f5e927b1122c8b608f11a5f5bc97

doc/build/changelog/unreleased_20/12829.rst
doc/build/orm/declarative_tables.rst
test/orm/declarative/test_tm_future_annotations_sync.py
test/orm/declarative/test_typed_mapping.py

index f307545c3a3e08e2a66c1a5937022dc9ad88e989..5dd8d3e9d4fae8f78506ad2a63d654985eab8877 100644 (file)
@@ -4,19 +4,20 @@
 
     The way ORM Annotated Declarative interprets Python :pep:`695` type aliases
     in ``Mapped[]`` annotations has been refined to expand the lookup scheme. A
-    PEP 695 type can now be resolved based on either its direct presence in
+    :pep:`695` type can now be resolved based on either its direct presence in
     :paramref:`_orm.registry.type_annotation_map` or its immediate resolved
-    value, as long as a recursive lookup across multiple pep-695 types is not
-    required for it to resolve. This change reverses part of the restrictions
-    introduced in 2.0.37 as part of :ticket:`11955`, which deprecated (and
-    disallowed in 2.1) the ability to resolve any PEP 695 type that was not
-    explicitly present in :paramref:`_orm.registry.type_annotation_map`.
-    Recursive lookups of PEP 695 types remains deprecated in 2.0 and disallowed
-    in version 2.1, as do implicit lookups of ``NewType`` types without an
-    entry in :paramref:`_orm.registry.type_annotation_map`.
+    value, as long as a recursive lookup across multiple :pep:`695` types is
+    not required for it to resolve. This change reverses part of the
+    restrictions introduced in 2.0.37 as part of :ticket:`11955`, which
+    deprecated (and disallowed in 2.1) the ability to resolve any :pep:`695`
+    type that was not explicitly present in
+    :paramref:`_orm.registry.type_annotation_map`. Recursive lookups of
+    :pep:`695` types remains deprecated in 2.0 and disallowed in version 2.1,
+    as do implicit lookups of ``NewType`` types without an entry in
+    :paramref:`_orm.registry.type_annotation_map`.
 
-    Additionally, new support has been added for generic PEP 695 aliases that
-    refer to PEP 593 ``Annotated`` constructs containing
+    Additionally, new support has been added for generic :pep:`695` aliases that
+    refer to :pep:`593` ``Annotated`` constructs containing
     :func:`_orm.mapped_column` configurations. See the sections below for
     examples.
 
index dff21e776bf46b12cbd6c3a7de0c204436381765..e56ac8a51f4b3a33643fc0b0b26ba4140cca1986 100644 (file)
@@ -1246,7 +1246,7 @@ adding a ``FOREIGN KEY`` constraint as well as substituting
 .. _orm_declarative_mapped_column_generic_pep593:
 
 Mapping Whole Column Declarations to Generic Python Types
-^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
 Using the ``Annotated`` approach from the previous section, we may also
 create a generic version that will apply particular :func:`_orm.mapped_column`
@@ -1283,12 +1283,13 @@ The above type can now apply ``primary_key=True`` to any Python type::
         # will create a UUID primary key
         id: Mapped[PrimaryKey[uuid.UUID]]
 
-The type alias may also be defined equivalently using the pep-695 ``type``
-keyword in Python 3.12 or above::
+For a more shorthand approach, we may opt to use the :pep:`695` ``type``
+keyword (Python 3.12 or above) which allows us to skip having to define a
+``TypeVar`` variable::
 
     type PrimaryKey[T] = Annotated[T, mapped_column(primary_key=True)]
 
-.. versionadded:: 2.0.44 Generic pep-695 types may be used with pep-593
+.. versionadded:: 2.0.44 Generic :pep:`695` types may be used with :pep:`593`
     ``Annotated`` elements to create generic types that automatically
     deliver :func:`_orm.mapped_column` arguments.
 
index 960f4c0c9c394d00cf74bc19db5f5a99caa12a16..5f193693e306b17bcd84c1b5cf201368bae122c5 100644 (file)
@@ -29,7 +29,6 @@ from typing import Optional
 from typing import Set
 from typing import Type
 from typing import TYPE_CHECKING
-from typing import TypeAlias as TypeAlias
 from typing import TypedDict
 from typing import TypeVar
 from typing import Union
@@ -104,78 +103,82 @@ from sqlalchemy.testing import Variation
 from sqlalchemy.testing.assertions import ne_
 from sqlalchemy.testing.fixtures import fixture_session
 
-TV = typing.TypeVar("TV")
 
+# try to differentiate between typing_extensions.TypeAliasType
+# and typing.TypeAliasType
+TypingTypeAliasType = getattr(typing, "TypeAliasType", TypeAliasType)
+
+
+@testing.fixture
+def pep_695_types():
+    global TV
+    global _UnionPep695, _TypingGenericPep695, _TypingStrPep695
+    global _TypingGenericPep695, _TypingGenericPep695Typed
+    global _TypingLiteral695
+    global _Literal695, _RecursiveLiteral695
+    global _StrPep695, _GenericPep695, _GenericPep695Typed, _GenericPep695Typed
 
-class _SomeDict1(TypedDict):
-    type: Literal["1"]
+    TV = typing.TypeVar("TV")
 
+    _StrPep695 = TypeAliasType("_StrPep695", str)  # type: ignore
+    _GenericPep695 = TypeAliasType(  # type: ignore
+        "_GenericPep695", List[TV], type_params=(TV,)
+    )
+    _GenericPep695Typed = _GenericPep695[int]
 
-class _SomeDict2(TypedDict):
-    type: Literal["2"]
+    class _SomeDict1(TypedDict):
+        type: Literal["1"]
 
+    class _SomeDict2(TypedDict):
+        type: Literal["2"]
 
-_UnionTypeAlias: TypeAlias = Union[_SomeDict1, _SomeDict2]
+    _Literal695 = TypeAliasType(  # type: ignore
+        "_Literal695", Literal["to-do", "in-progress", "done"]
+    )
+    _RecursiveLiteral695 = TypeAliasType(  # type: ignore
+        "_RecursiveLiteral695", _Literal695
+    )
 
-_StrTypeAlias: TypeAlias = str
+    _UnionPep695 = TypeAliasType(  # type: ignore
+        "_UnionPep695", Union[_SomeDict1, _SomeDict2]
+    )
+    _TypingStrPep695 = TypingTypeAliasType(  # type: ignore
+        "_TypingStrPep695", str
+    )
 
+    _TypingGenericPep695 = TypingTypeAliasType(  # type: ignore
+        "_TypingGenericPep695", List[TV], type_params=(TV,)  # type: ignore
+    )
 
-_TypingLiteral = typing.Literal["a", "b"]
-_TypingExtensionsLiteral = typing_extensions.Literal["a", "b"]
+    _TypingGenericPep695Typed = _TypingGenericPep695[int]  # type: ignore
 
-_JsonPrimitive: TypeAlias = Union[str, int, float, bool, None]
-_JsonObject: TypeAlias = Dict[str, "_Json"]
-_JsonArray: TypeAlias = List["_Json"]
-_Json: TypeAlias = Union[_JsonObject, _JsonArray, _JsonPrimitive]
-_JsonPrimitivePep604: TypeAlias = str | int | float | bool | None
-_JsonObjectPep604: TypeAlias = dict[str, "_JsonPep604"]
-_JsonArrayPep604: TypeAlias = list["_JsonPep604"]
-_JsonPep604: TypeAlias = (
-    _JsonObjectPep604 | _JsonArrayPep604 | _JsonPrimitivePep604
-)
-_JsonPep695 = TypeAliasType("_JsonPep695", _JsonPep604)
+    _TypingLiteral695 = TypingTypeAliasType(  # type: ignore
+        "_TypingLiteral695", Literal["to-do", "in-progress", "done"]
+    )
 
 
-TypingTypeAliasType = getattr(typing, "TypeAliasType", TypeAliasType)
+@testing.fixture
+def pep_593_types(pep_695_types):
+    global _GenericPep593TypeAlias, _GenericPep593Pep695
+    global _RecursivePep695Pep593
 
-_StrPep695 = TypeAliasType("_StrPep695", str)
-_TypingStrPep695 = TypingTypeAliasType("_TypingStrPep695", str)
-_GenericPep695 = TypeAliasType("_GenericPep695", List[TV], type_params=(TV,))
-_TypingGenericPep695 = TypingTypeAliasType(
-    "_TypingGenericPep695", List[TV], type_params=(TV,)
-)
-_GenericPep695Typed = _GenericPep695[int]
-_TypingGenericPep695Typed = _TypingGenericPep695[int]
-_UnionPep695 = TypeAliasType("_UnionPep695", Union[_SomeDict1, _SomeDict2])
-strtypalias_keyword = TypeAliasType(
-    "strtypalias_keyword", Annotated[str, mapped_column(info={"hi": "there"})]
-)
-strtypalias_keyword_nested = TypeAliasType(
-    "strtypalias_keyword_nested",
-    int | Annotated[str, mapped_column(info={"hi": "there"})],
-)
-strtypalias_ta: TypeAlias = Annotated[str, mapped_column(info={"hi": "there"})]
-strtypalias_plain = Annotated[str, mapped_column(info={"hi": "there"})]
-_Literal695 = TypeAliasType(
-    "_Literal695", Literal["to-do", "in-progress", "done"]
-)
-_TypingLiteral695 = TypingTypeAliasType(
-    "_TypingLiteral695", Literal["to-do", "in-progress", "done"]
-)
-_RecursiveLiteral695 = TypeAliasType("_RecursiveLiteral695", _Literal695)
-
-_GenericPep593TypeAlias = Annotated[TV, mapped_column(info={"hi": "there"})]
-
-_GenericPep593Pep695 = TypingTypeAliasType(
-    "_GenericPep593Pep695",
-    Annotated[TV, mapped_column(info={"hi": "there"})],
-    type_params=(TV,),
-)
-
-_RecursivePep695Pep593 = TypingTypeAliasType(
-    "_RecursivePep695Pep593",
-    Annotated[_TypingStrPep695, mapped_column(info={"hi": "there"})],
-)
+    _GenericPep593TypeAlias = Annotated[
+        TV, mapped_column(info={"hi": "there"})  # type: ignore
+    ]
+
+    _GenericPep593Pep695 = TypingTypeAliasType(  # type: ignore
+        "_GenericPep593Pep695",
+        Annotated[TV, mapped_column(info={"hi": "there"})],  # type: ignore
+        type_params=(TV,),
+    )
+
+    _RecursivePep695Pep593 = TypingTypeAliasType(  # type: ignore
+        "_RecursivePep695Pep593",
+        Annotated[
+            _TypingStrPep695,  # type: ignore
+            mapped_column(info={"hi": "there"}),
+        ],
+    )
 
 
 def expect_annotation_syntax_error(name):
@@ -219,15 +222,6 @@ class DeclarativeBaseTest(fixtures.TestBase):
             Tab.non_existent
 
 
-_annotated_names_tested = set()
-
-
-def annotated_name_test_cases(*cases, **kw):
-    _annotated_names_tested.update([case[0] for case in cases])
-
-    return testing.combinations_list(cases, **kw)
-
-
 class MappedColumnTest(fixtures.TestBase, testing.AssertsCompiledSQL):
     __dialect__ = "default"
 
@@ -632,7 +626,7 @@ class MappedColumnTest(fixtures.TestBase, testing.AssertsCompiledSQL):
         anno_str = Annotated[str, 50]
         anno_str_optional = Annotated[Optional[str], 30]
 
-        newtype_str = NewType("MyType", str)
+        newtype_str = NewType("newtype_str", str)
 
         anno_str_mc = Annotated[str, mapped_column()]
         anno_str_optional_mc = Annotated[Optional[str], mapped_column()]
@@ -771,6 +765,11 @@ class MappedColumnTest(fixtures.TestBase, testing.AssertsCompiledSQL):
     def test_typing_literal_identity(self, decl_base):
         """See issue #11820"""
 
+        global _TypingLiteral, _TypingExtensionsLiteral
+
+        _TypingLiteral = typing.Literal["a", "b"]
+        _TypingExtensionsLiteral = typing_extensions.Literal["a", "b"]
+
         class Foo(decl_base):
             __tablename__ = "footable"
 
@@ -783,159 +782,176 @@ class MappedColumnTest(fixtures.TestBase, testing.AssertsCompiledSQL):
             eq_(col.type.enums, ["a", "b"])
             is_(col.type.native_enum, False)
 
-    def test_we_got_all_attrs_test_annotated(self):
-        argnames = _py_inspect.getfullargspec(mapped_column)
-        assert _annotated_names_tested.issuperset(argnames.kwonlyargs), (
-            f"annotated attributes were not tested: "
-            f"{set(argnames.kwonlyargs).difference(_annotated_names_tested)}"
-        )
-
-    @annotated_name_test_cases(
-        ("sort_order", 100, lambda sort_order: sort_order == 100),
-        ("nullable", False, lambda column: column.nullable is False),
-        (
-            "active_history",
-            True,
-            lambda column_property: column_property.active_history is True,
-        ),
-        (
-            "deferred",
-            True,
-            lambda column_property: column_property.deferred is True,
-        ),
-        (
-            "deferred",
-            _NoArg.NO_ARG,
-            lambda column_property: column_property is None,
-        ),
-        (
-            "deferred_group",
-            "mygroup",
-            lambda column_property: column_property.deferred is True
-            and column_property.group == "mygroup",
-        ),
-        (
-            "deferred_raiseload",
-            True,
-            lambda column_property: column_property.deferred is True
-            and column_property.raiseload is True,
-        ),
-        (
-            "server_default",
-            "25",
-            lambda column: column.server_default.arg == "25",
-        ),
-        (
-            "server_onupdate",
-            "25",
-            lambda column: column.server_onupdate.arg == "25",
-        ),
-        (
-            "default",
-            25,
-            lambda column: column.default.arg == 25,
-        ),
-        (
-            "insert_default",
-            25,
-            lambda column: column.default.arg == 25,
-        ),
-        (
-            "onupdate",
-            25,
-            lambda column: column.onupdate.arg == 25,
-        ),
-        ("doc", "some doc", lambda column: column.doc == "some doc"),
-        (
-            "comment",
-            "some comment",
-            lambda column: column.comment == "some comment",
-        ),
-        ("index", True, lambda column: column.index is True),
-        ("index", _NoArg.NO_ARG, lambda column: column.index is None),
-        ("index", False, lambda column: column.index is False),
-        ("unique", True, lambda column: column.unique is True),
-        ("unique", False, lambda column: column.unique is False),
-        ("autoincrement", True, lambda column: column.autoincrement is True),
-        ("system", True, lambda column: column.system is True),
-        ("primary_key", True, lambda column: column.primary_key is True),
-        ("type_", BIGINT, lambda column: isinstance(column.type, BIGINT)),
-        ("info", {"foo": "bar"}, lambda column: column.info == {"foo": "bar"}),
-        (
-            "use_existing_column",
-            True,
-            lambda mc: mc._use_existing_column is True,
-        ),
-        (
-            "quote",
-            True,
-            exc.SADeprecationWarning(
-                "Can't use the 'key' or 'name' arguments in Annotated "
+    @staticmethod
+    def annotated_name_test_cases():
+        return [
+            ("sort_order", 100, lambda sort_order: sort_order == 100),
+            ("nullable", False, lambda column: column.nullable is False),
+            (
+                "active_history",
+                True,
+                lambda column_property: column_property.active_history is True,
             ),
-        ),
-        (
-            "key",
-            "mykey",
-            exc.SADeprecationWarning(
-                "Can't use the 'key' or 'name' arguments in Annotated "
+            (
+                "deferred",
+                True,
+                lambda column_property: column_property.deferred is True,
             ),
-        ),
-        (
-            "name",
-            "mykey",
-            exc.SADeprecationWarning(
-                "Can't use the 'key' or 'name' arguments in Annotated "
+            (
+                "deferred",
+                _NoArg.NO_ARG,
+                lambda column_property: column_property is None,
             ),
-        ),
-        (
-            "kw_only",
-            True,
-            exc.SADeprecationWarning(
-                "Argument 'kw_only' is a dataclass argument "
+            (
+                "deferred_group",
+                "mygroup",
+                lambda column_property: column_property.deferred is True
+                and column_property.group == "mygroup",
             ),
-        ),
-        (
-            "compare",
-            True,
-            exc.SADeprecationWarning(
-                "Argument 'compare' is a dataclass argument "
+            (
+                "deferred_raiseload",
+                True,
+                lambda column_property: column_property.deferred is True
+                and column_property.raiseload is True,
             ),
-        ),
-        (
-            "default_factory",
-            lambda: 25,
-            exc.SADeprecationWarning(
-                "Argument 'default_factory' is a dataclass argument "
+            (
+                "server_default",
+                "25",
+                lambda column: column.server_default.arg == "25",
             ),
-        ),
-        (
-            "repr",
-            True,
-            exc.SADeprecationWarning(
-                "Argument 'repr' is a dataclass argument "
+            (
+                "server_onupdate",
+                "25",
+                lambda column: column.server_onupdate.arg == "25",
             ),
-        ),
-        (
-            "init",
-            True,
-            exc.SADeprecationWarning(
-                "Argument 'init' is a dataclass argument"
+            (
+                "default",
+                25,
+                lambda column: column.default.arg == 25,
             ),
-        ),
-        (
-            "hash",
-            True,
-            exc.SADeprecationWarning(
-                "Argument 'hash' is a dataclass argument"
+            (
+                "insert_default",
+                25,
+                lambda column: column.default.arg == 25,
             ),
-        ),
-        (
-            "dataclass_metadata",
-            {},
-            exc.SADeprecationWarning(
-                "Argument 'dataclass_metadata' is a dataclass argument"
+            (
+                "onupdate",
+                25,
+                lambda column: column.onupdate.arg == 25,
             ),
-        ),
+            ("doc", "some doc", lambda column: column.doc == "some doc"),
+            (
+                "comment",
+                "some comment",
+                lambda column: column.comment == "some comment",
+            ),
+            ("index", True, lambda column: column.index is True),
+            ("index", _NoArg.NO_ARG, lambda column: column.index is None),
+            ("index", False, lambda column: column.index is False),
+            ("unique", True, lambda column: column.unique is True),
+            ("unique", False, lambda column: column.unique is False),
+            (
+                "autoincrement",
+                True,
+                lambda column: column.autoincrement is True,
+            ),
+            ("system", True, lambda column: column.system is True),
+            ("primary_key", True, lambda column: column.primary_key is True),
+            ("type_", BIGINT, lambda column: isinstance(column.type, BIGINT)),
+            (
+                "info",
+                {"foo": "bar"},
+                lambda column: column.info == {"foo": "bar"},
+            ),
+            (
+                "use_existing_column",
+                True,
+                lambda mc: mc._use_existing_column is True,
+            ),
+            (
+                "quote",
+                True,
+                exc.SADeprecationWarning(
+                    "Can't use the 'key' or 'name' arguments in Annotated "
+                ),
+            ),
+            (
+                "key",
+                "mykey",
+                exc.SADeprecationWarning(
+                    "Can't use the 'key' or 'name' arguments in Annotated "
+                ),
+            ),
+            (
+                "name",
+                "mykey",
+                exc.SADeprecationWarning(
+                    "Can't use the 'key' or 'name' arguments in Annotated "
+                ),
+            ),
+            (
+                "kw_only",
+                True,
+                exc.SADeprecationWarning(
+                    "Argument 'kw_only' is a dataclass argument "
+                ),
+            ),
+            (
+                "compare",
+                True,
+                exc.SADeprecationWarning(
+                    "Argument 'compare' is a dataclass argument "
+                ),
+            ),
+            (
+                "default_factory",
+                lambda: 25,
+                exc.SADeprecationWarning(
+                    "Argument 'default_factory' is a dataclass argument "
+                ),
+            ),
+            (
+                "repr",
+                True,
+                exc.SADeprecationWarning(
+                    "Argument 'repr' is a dataclass argument "
+                ),
+            ),
+            (
+                "init",
+                True,
+                exc.SADeprecationWarning(
+                    "Argument 'init' is a dataclass argument"
+                ),
+            ),
+            (
+                "hash",
+                True,
+                exc.SADeprecationWarning(
+                    "Argument 'hash' is a dataclass argument"
+                ),
+            ),
+            (
+                "dataclass_metadata",
+                {},
+                exc.SADeprecationWarning(
+                    "Argument 'dataclass_metadata' is a dataclass argument"
+                ),
+            ),
+        ]
+
+    def test_we_got_all_attrs_test_annotated(self):
+        argnames = _py_inspect.getfullargspec(mapped_column)
+        _annotated_names_tested = {
+            case[0] for case in self.annotated_name_test_cases()
+        }
+        assert _annotated_names_tested.issuperset(argnames.kwonlyargs), (
+            f"annotated attributes were not tested: "
+            f"{set(argnames.kwonlyargs).difference(_annotated_names_tested)}"
+        )
+
+    @testing.combinations_list(
+        annotated_name_test_cases(),
         argnames="argname, argument, assertion",
     )
     @testing.variation("use_annotated", [True, False, "control"])
@@ -1222,6 +1238,21 @@ class Pep593InterpretationTests(fixtures.TestBase, testing.AssertsCompiledSQL):
         self, decl_base: Type[DeclarativeBase], alias_type
     ):
         """test #11130"""
+
+        global strtypalias_keyword, strtypalias_keyword_nested
+        global strtypalias_ta, strtypalias_plain
+
+        strtypalias_keyword = TypeAliasType(
+            "strtypalias_keyword",
+            Annotated[str, mapped_column(info={"hi": "there"})],
+        )
+        strtypalias_keyword_nested = TypeAliasType(
+            "strtypalias_keyword_nested",
+            int | Annotated[str, mapped_column(info={"hi": "there"})],
+        )
+        strtypalias_ta = Annotated[str, mapped_column(info={"hi": "there"})]
+        strtypalias_plain = Annotated[str, mapped_column(info={"hi": "there"})]
+
         if alias_type.typekeyword:
             decl_base.registry.update_type_annotation_map(
                 {strtypalias_keyword: VARCHAR(33)}  # noqa: F821
@@ -1265,8 +1296,9 @@ class Pep593InterpretationTests(fixtures.TestBase, testing.AssertsCompiledSQL):
 
     @testing.requires.python312
     def test_no_recursive_pep593_from_pep695(
-        self, decl_base: Type[DeclarativeBase]
+        self, decl_base: Type[DeclarativeBase], pep_593_types
     ):
+
         def declare():
             class MyClass(decl_base):
                 __tablename__ = "my_table"
@@ -1800,7 +1832,11 @@ class Pep593InterpretationTests(fixtures.TestBase, testing.AssertsCompiledSQL):
     @testing.variation("alias_type", ["plain", "pep695"])
     @testing.requires.python312
     def test_generic_typealias_pep593(
-        self, decl_base: Type[DeclarativeBase], alias_type: Variation, in_map
+        self,
+        decl_base: Type[DeclarativeBase],
+        alias_type: Variation,
+        in_map,
+        pep_593_types,
     ):
 
         if in_map:
@@ -2167,6 +2203,18 @@ class TypeResolutionTests(fixtures.TestBase, testing.AssertsCompiledSQL):
     def test_plain_typealias_as_typemap_keys(
         self, decl_base: Type[DeclarativeBase]
     ):
+
+        global _StrTypeAlias, _UnionTypeAlias
+
+        class _SomeDict1(TypedDict):
+            type: Literal["1"]
+
+        class _SomeDict2(TypedDict):
+            type: Literal["2"]
+
+        _StrTypeAlias = str
+        _UnionTypeAlias = Union[_SomeDict1, _SomeDict2]
+
         decl_base.registry.update_type_annotation_map(
             {_UnionTypeAlias: JSON, _StrTypeAlias: String(30)}
         )
@@ -2336,7 +2384,7 @@ class TypeResolutionTests(fixtures.TestBase, testing.AssertsCompiledSQL):
     )
     @testing.requires.python312
     def test_pep695_typealias_as_typemap_keys(
-        self, decl_base: Type[DeclarativeBase], type_
+        self, decl_base: Type[DeclarativeBase], type_, pep_695_types
     ):
         """test #10807, #12829"""
 
@@ -2534,6 +2582,20 @@ class TypeResolutionTests(fixtures.TestBase, testing.AssertsCompiledSQL):
     def test_optional_in_annotation_map(self, union):
         """See issue #11370"""
 
+        global _Json, _JsonPep604, _JsonPep695
+
+        _JsonPrimitive = Union[str, int, float, bool, None]
+        _JsonObject = Dict[str, "_Json"]
+        _JsonArray = List["_Json"]
+        _Json = Union[_JsonObject, _JsonArray, _JsonPrimitive]
+        _JsonPrimitivePep604 = str | int | float | bool | None
+        _JsonObjectPep604 = dict[str, "_JsonPep604"]
+        _JsonArrayPep604 = list["_JsonPep604"]
+        _JsonPep604 = (
+            _JsonObjectPep604 | _JsonArrayPep604 | _JsonPrimitivePep604
+        )
+        _JsonPep695 = TypeAliasType("_JsonPep695", _JsonPep604)
+
         class Base(DeclarativeBase):
             if union.union:
                 type_annotation_map = {_Json: JSON}
@@ -2871,7 +2933,9 @@ class ResolveToEnumTest(fixtures.TestBase, testing.AssertsCompiledSQL):
     )
     @testing.combinations(True, False, argnames="in_map")
     @testing.requires.python312
-    def test_pep695_literal_defaults_to_enum(self, decl_base, type_, in_map):
+    def test_pep695_literal_defaults_to_enum(
+        self, decl_base, type_, in_map, pep_695_types
+    ):
         """test #11305."""
 
         def declare():
index 0aa0ded6a0ab98cabd1417a05c375968e6ba6711..eb8ac576f7e6d685475ef568dd856317c8980da3 100644 (file)
@@ -20,7 +20,6 @@ from typing import Optional
 from typing import Set
 from typing import Type
 from typing import TYPE_CHECKING
-from typing import TypeAlias as TypeAlias
 from typing import TypedDict
 from typing import TypeVar
 from typing import Union
@@ -95,78 +94,82 @@ from sqlalchemy.testing import Variation
 from sqlalchemy.testing.assertions import ne_
 from sqlalchemy.testing.fixtures import fixture_session
 
-TV = typing.TypeVar("TV")
 
+# try to differentiate between typing_extensions.TypeAliasType
+# and typing.TypeAliasType
+TypingTypeAliasType = getattr(typing, "TypeAliasType", TypeAliasType)
+
+
+@testing.fixture
+def pep_695_types():
+    global TV
+    global _UnionPep695, _TypingGenericPep695, _TypingStrPep695
+    global _TypingGenericPep695, _TypingGenericPep695Typed
+    global _TypingLiteral695
+    global _Literal695, _RecursiveLiteral695
+    global _StrPep695, _GenericPep695, _GenericPep695Typed, _GenericPep695Typed
 
-class _SomeDict1(TypedDict):
-    type: Literal["1"]
+    TV = typing.TypeVar("TV")
 
+    _StrPep695 = TypeAliasType("_StrPep695", str)  # type: ignore
+    _GenericPep695 = TypeAliasType(  # type: ignore
+        "_GenericPep695", List[TV], type_params=(TV,)
+    )
+    _GenericPep695Typed = _GenericPep695[int]
 
-class _SomeDict2(TypedDict):
-    type: Literal["2"]
+    class _SomeDict1(TypedDict):
+        type: Literal["1"]
 
+    class _SomeDict2(TypedDict):
+        type: Literal["2"]
 
-_UnionTypeAlias: TypeAlias = Union[_SomeDict1, _SomeDict2]
+    _Literal695 = TypeAliasType(  # type: ignore
+        "_Literal695", Literal["to-do", "in-progress", "done"]
+    )
+    _RecursiveLiteral695 = TypeAliasType(  # type: ignore
+        "_RecursiveLiteral695", _Literal695
+    )
 
-_StrTypeAlias: TypeAlias = str
+    _UnionPep695 = TypeAliasType(  # type: ignore
+        "_UnionPep695", Union[_SomeDict1, _SomeDict2]
+    )
+    _TypingStrPep695 = TypingTypeAliasType(  # type: ignore
+        "_TypingStrPep695", str
+    )
 
+    _TypingGenericPep695 = TypingTypeAliasType(  # type: ignore
+        "_TypingGenericPep695", List[TV], type_params=(TV,)  # type: ignore
+    )
 
-_TypingLiteral = typing.Literal["a", "b"]
-_TypingExtensionsLiteral = typing_extensions.Literal["a", "b"]
+    _TypingGenericPep695Typed = _TypingGenericPep695[int]  # type: ignore
 
-_JsonPrimitive: TypeAlias = Union[str, int, float, bool, None]
-_JsonObject: TypeAlias = Dict[str, "_Json"]
-_JsonArray: TypeAlias = List["_Json"]
-_Json: TypeAlias = Union[_JsonObject, _JsonArray, _JsonPrimitive]
-_JsonPrimitivePep604: TypeAlias = str | int | float | bool | None
-_JsonObjectPep604: TypeAlias = dict[str, "_JsonPep604"]
-_JsonArrayPep604: TypeAlias = list["_JsonPep604"]
-_JsonPep604: TypeAlias = (
-    _JsonObjectPep604 | _JsonArrayPep604 | _JsonPrimitivePep604
-)
-_JsonPep695 = TypeAliasType("_JsonPep695", _JsonPep604)
+    _TypingLiteral695 = TypingTypeAliasType(  # type: ignore
+        "_TypingLiteral695", Literal["to-do", "in-progress", "done"]
+    )
 
 
-TypingTypeAliasType = getattr(typing, "TypeAliasType", TypeAliasType)
+@testing.fixture
+def pep_593_types(pep_695_types):
+    global _GenericPep593TypeAlias, _GenericPep593Pep695
+    global _RecursivePep695Pep593
 
-_StrPep695 = TypeAliasType("_StrPep695", str)
-_TypingStrPep695 = TypingTypeAliasType("_TypingStrPep695", str)
-_GenericPep695 = TypeAliasType("_GenericPep695", List[TV], type_params=(TV,))
-_TypingGenericPep695 = TypingTypeAliasType(
-    "_TypingGenericPep695", List[TV], type_params=(TV,)
-)
-_GenericPep695Typed = _GenericPep695[int]
-_TypingGenericPep695Typed = _TypingGenericPep695[int]
-_UnionPep695 = TypeAliasType("_UnionPep695", Union[_SomeDict1, _SomeDict2])
-strtypalias_keyword = TypeAliasType(
-    "strtypalias_keyword", Annotated[str, mapped_column(info={"hi": "there"})]
-)
-strtypalias_keyword_nested = TypeAliasType(
-    "strtypalias_keyword_nested",
-    int | Annotated[str, mapped_column(info={"hi": "there"})],
-)
-strtypalias_ta: TypeAlias = Annotated[str, mapped_column(info={"hi": "there"})]
-strtypalias_plain = Annotated[str, mapped_column(info={"hi": "there"})]
-_Literal695 = TypeAliasType(
-    "_Literal695", Literal["to-do", "in-progress", "done"]
-)
-_TypingLiteral695 = TypingTypeAliasType(
-    "_TypingLiteral695", Literal["to-do", "in-progress", "done"]
-)
-_RecursiveLiteral695 = TypeAliasType("_RecursiveLiteral695", _Literal695)
-
-_GenericPep593TypeAlias = Annotated[TV, mapped_column(info={"hi": "there"})]
-
-_GenericPep593Pep695 = TypingTypeAliasType(
-    "_GenericPep593Pep695",
-    Annotated[TV, mapped_column(info={"hi": "there"})],
-    type_params=(TV,),
-)
-
-_RecursivePep695Pep593 = TypingTypeAliasType(
-    "_RecursivePep695Pep593",
-    Annotated[_TypingStrPep695, mapped_column(info={"hi": "there"})],
-)
+    _GenericPep593TypeAlias = Annotated[
+        TV, mapped_column(info={"hi": "there"})  # type: ignore
+    ]
+
+    _GenericPep593Pep695 = TypingTypeAliasType(  # type: ignore
+        "_GenericPep593Pep695",
+        Annotated[TV, mapped_column(info={"hi": "there"})],  # type: ignore
+        type_params=(TV,),
+    )
+
+    _RecursivePep695Pep593 = TypingTypeAliasType(  # type: ignore
+        "_RecursivePep695Pep593",
+        Annotated[
+            _TypingStrPep695,  # type: ignore
+            mapped_column(info={"hi": "there"}),
+        ],
+    )
 
 
 def expect_annotation_syntax_error(name):
@@ -210,15 +213,6 @@ class DeclarativeBaseTest(fixtures.TestBase):
             Tab.non_existent
 
 
-_annotated_names_tested = set()
-
-
-def annotated_name_test_cases(*cases, **kw):
-    _annotated_names_tested.update([case[0] for case in cases])
-
-    return testing.combinations_list(cases, **kw)
-
-
 class MappedColumnTest(fixtures.TestBase, testing.AssertsCompiledSQL):
     __dialect__ = "default"
 
@@ -623,7 +617,7 @@ class MappedColumnTest(fixtures.TestBase, testing.AssertsCompiledSQL):
         anno_str = Annotated[str, 50]
         anno_str_optional = Annotated[Optional[str], 30]
 
-        newtype_str = NewType("MyType", str)
+        newtype_str = NewType("newtype_str", str)
 
         anno_str_mc = Annotated[str, mapped_column()]
         anno_str_optional_mc = Annotated[Optional[str], mapped_column()]
@@ -762,6 +756,11 @@ class MappedColumnTest(fixtures.TestBase, testing.AssertsCompiledSQL):
     def test_typing_literal_identity(self, decl_base):
         """See issue #11820"""
 
+        # anno only: global _TypingLiteral, _TypingExtensionsLiteral
+
+        _TypingLiteral = typing.Literal["a", "b"]
+        _TypingExtensionsLiteral = typing_extensions.Literal["a", "b"]
+
         class Foo(decl_base):
             __tablename__ = "footable"
 
@@ -774,159 +773,176 @@ class MappedColumnTest(fixtures.TestBase, testing.AssertsCompiledSQL):
             eq_(col.type.enums, ["a", "b"])
             is_(col.type.native_enum, False)
 
-    def test_we_got_all_attrs_test_annotated(self):
-        argnames = _py_inspect.getfullargspec(mapped_column)
-        assert _annotated_names_tested.issuperset(argnames.kwonlyargs), (
-            f"annotated attributes were not tested: "
-            f"{set(argnames.kwonlyargs).difference(_annotated_names_tested)}"
-        )
-
-    @annotated_name_test_cases(
-        ("sort_order", 100, lambda sort_order: sort_order == 100),
-        ("nullable", False, lambda column: column.nullable is False),
-        (
-            "active_history",
-            True,
-            lambda column_property: column_property.active_history is True,
-        ),
-        (
-            "deferred",
-            True,
-            lambda column_property: column_property.deferred is True,
-        ),
-        (
-            "deferred",
-            _NoArg.NO_ARG,
-            lambda column_property: column_property is None,
-        ),
-        (
-            "deferred_group",
-            "mygroup",
-            lambda column_property: column_property.deferred is True
-            and column_property.group == "mygroup",
-        ),
-        (
-            "deferred_raiseload",
-            True,
-            lambda column_property: column_property.deferred is True
-            and column_property.raiseload is True,
-        ),
-        (
-            "server_default",
-            "25",
-            lambda column: column.server_default.arg == "25",
-        ),
-        (
-            "server_onupdate",
-            "25",
-            lambda column: column.server_onupdate.arg == "25",
-        ),
-        (
-            "default",
-            25,
-            lambda column: column.default.arg == 25,
-        ),
-        (
-            "insert_default",
-            25,
-            lambda column: column.default.arg == 25,
-        ),
-        (
-            "onupdate",
-            25,
-            lambda column: column.onupdate.arg == 25,
-        ),
-        ("doc", "some doc", lambda column: column.doc == "some doc"),
-        (
-            "comment",
-            "some comment",
-            lambda column: column.comment == "some comment",
-        ),
-        ("index", True, lambda column: column.index is True),
-        ("index", _NoArg.NO_ARG, lambda column: column.index is None),
-        ("index", False, lambda column: column.index is False),
-        ("unique", True, lambda column: column.unique is True),
-        ("unique", False, lambda column: column.unique is False),
-        ("autoincrement", True, lambda column: column.autoincrement is True),
-        ("system", True, lambda column: column.system is True),
-        ("primary_key", True, lambda column: column.primary_key is True),
-        ("type_", BIGINT, lambda column: isinstance(column.type, BIGINT)),
-        ("info", {"foo": "bar"}, lambda column: column.info == {"foo": "bar"}),
-        (
-            "use_existing_column",
-            True,
-            lambda mc: mc._use_existing_column is True,
-        ),
-        (
-            "quote",
-            True,
-            exc.SADeprecationWarning(
-                "Can't use the 'key' or 'name' arguments in Annotated "
+    @staticmethod
+    def annotated_name_test_cases():
+        return [
+            ("sort_order", 100, lambda sort_order: sort_order == 100),
+            ("nullable", False, lambda column: column.nullable is False),
+            (
+                "active_history",
+                True,
+                lambda column_property: column_property.active_history is True,
             ),
-        ),
-        (
-            "key",
-            "mykey",
-            exc.SADeprecationWarning(
-                "Can't use the 'key' or 'name' arguments in Annotated "
+            (
+                "deferred",
+                True,
+                lambda column_property: column_property.deferred is True,
             ),
-        ),
-        (
-            "name",
-            "mykey",
-            exc.SADeprecationWarning(
-                "Can't use the 'key' or 'name' arguments in Annotated "
+            (
+                "deferred",
+                _NoArg.NO_ARG,
+                lambda column_property: column_property is None,
             ),
-        ),
-        (
-            "kw_only",
-            True,
-            exc.SADeprecationWarning(
-                "Argument 'kw_only' is a dataclass argument "
+            (
+                "deferred_group",
+                "mygroup",
+                lambda column_property: column_property.deferred is True
+                and column_property.group == "mygroup",
             ),
-        ),
-        (
-            "compare",
-            True,
-            exc.SADeprecationWarning(
-                "Argument 'compare' is a dataclass argument "
+            (
+                "deferred_raiseload",
+                True,
+                lambda column_property: column_property.deferred is True
+                and column_property.raiseload is True,
             ),
-        ),
-        (
-            "default_factory",
-            lambda: 25,
-            exc.SADeprecationWarning(
-                "Argument 'default_factory' is a dataclass argument "
+            (
+                "server_default",
+                "25",
+                lambda column: column.server_default.arg == "25",
             ),
-        ),
-        (
-            "repr",
-            True,
-            exc.SADeprecationWarning(
-                "Argument 'repr' is a dataclass argument "
+            (
+                "server_onupdate",
+                "25",
+                lambda column: column.server_onupdate.arg == "25",
             ),
-        ),
-        (
-            "init",
-            True,
-            exc.SADeprecationWarning(
-                "Argument 'init' is a dataclass argument"
+            (
+                "default",
+                25,
+                lambda column: column.default.arg == 25,
             ),
-        ),
-        (
-            "hash",
-            True,
-            exc.SADeprecationWarning(
-                "Argument 'hash' is a dataclass argument"
+            (
+                "insert_default",
+                25,
+                lambda column: column.default.arg == 25,
             ),
-        ),
-        (
-            "dataclass_metadata",
-            {},
-            exc.SADeprecationWarning(
-                "Argument 'dataclass_metadata' is a dataclass argument"
+            (
+                "onupdate",
+                25,
+                lambda column: column.onupdate.arg == 25,
             ),
-        ),
+            ("doc", "some doc", lambda column: column.doc == "some doc"),
+            (
+                "comment",
+                "some comment",
+                lambda column: column.comment == "some comment",
+            ),
+            ("index", True, lambda column: column.index is True),
+            ("index", _NoArg.NO_ARG, lambda column: column.index is None),
+            ("index", False, lambda column: column.index is False),
+            ("unique", True, lambda column: column.unique is True),
+            ("unique", False, lambda column: column.unique is False),
+            (
+                "autoincrement",
+                True,
+                lambda column: column.autoincrement is True,
+            ),
+            ("system", True, lambda column: column.system is True),
+            ("primary_key", True, lambda column: column.primary_key is True),
+            ("type_", BIGINT, lambda column: isinstance(column.type, BIGINT)),
+            (
+                "info",
+                {"foo": "bar"},
+                lambda column: column.info == {"foo": "bar"},
+            ),
+            (
+                "use_existing_column",
+                True,
+                lambda mc: mc._use_existing_column is True,
+            ),
+            (
+                "quote",
+                True,
+                exc.SADeprecationWarning(
+                    "Can't use the 'key' or 'name' arguments in Annotated "
+                ),
+            ),
+            (
+                "key",
+                "mykey",
+                exc.SADeprecationWarning(
+                    "Can't use the 'key' or 'name' arguments in Annotated "
+                ),
+            ),
+            (
+                "name",
+                "mykey",
+                exc.SADeprecationWarning(
+                    "Can't use the 'key' or 'name' arguments in Annotated "
+                ),
+            ),
+            (
+                "kw_only",
+                True,
+                exc.SADeprecationWarning(
+                    "Argument 'kw_only' is a dataclass argument "
+                ),
+            ),
+            (
+                "compare",
+                True,
+                exc.SADeprecationWarning(
+                    "Argument 'compare' is a dataclass argument "
+                ),
+            ),
+            (
+                "default_factory",
+                lambda: 25,
+                exc.SADeprecationWarning(
+                    "Argument 'default_factory' is a dataclass argument "
+                ),
+            ),
+            (
+                "repr",
+                True,
+                exc.SADeprecationWarning(
+                    "Argument 'repr' is a dataclass argument "
+                ),
+            ),
+            (
+                "init",
+                True,
+                exc.SADeprecationWarning(
+                    "Argument 'init' is a dataclass argument"
+                ),
+            ),
+            (
+                "hash",
+                True,
+                exc.SADeprecationWarning(
+                    "Argument 'hash' is a dataclass argument"
+                ),
+            ),
+            (
+                "dataclass_metadata",
+                {},
+                exc.SADeprecationWarning(
+                    "Argument 'dataclass_metadata' is a dataclass argument"
+                ),
+            ),
+        ]
+
+    def test_we_got_all_attrs_test_annotated(self):
+        argnames = _py_inspect.getfullargspec(mapped_column)
+        _annotated_names_tested = {
+            case[0] for case in self.annotated_name_test_cases()
+        }
+        assert _annotated_names_tested.issuperset(argnames.kwonlyargs), (
+            f"annotated attributes were not tested: "
+            f"{set(argnames.kwonlyargs).difference(_annotated_names_tested)}"
+        )
+
+    @testing.combinations_list(
+        annotated_name_test_cases(),
         argnames="argname, argument, assertion",
     )
     @testing.variation("use_annotated", [True, False, "control"])
@@ -1213,6 +1229,21 @@ class Pep593InterpretationTests(fixtures.TestBase, testing.AssertsCompiledSQL):
         self, decl_base: Type[DeclarativeBase], alias_type
     ):
         """test #11130"""
+
+        # anno only: global strtypalias_keyword, strtypalias_keyword_nested
+        # anno only: global strtypalias_ta, strtypalias_plain
+
+        strtypalias_keyword = TypeAliasType(
+            "strtypalias_keyword",
+            Annotated[str, mapped_column(info={"hi": "there"})],
+        )
+        strtypalias_keyword_nested = TypeAliasType(
+            "strtypalias_keyword_nested",
+            int | Annotated[str, mapped_column(info={"hi": "there"})],
+        )
+        strtypalias_ta = Annotated[str, mapped_column(info={"hi": "there"})]
+        strtypalias_plain = Annotated[str, mapped_column(info={"hi": "there"})]
+
         if alias_type.typekeyword:
             decl_base.registry.update_type_annotation_map(
                 {strtypalias_keyword: VARCHAR(33)}  # noqa: F821
@@ -1256,8 +1287,9 @@ class Pep593InterpretationTests(fixtures.TestBase, testing.AssertsCompiledSQL):
 
     @testing.requires.python312
     def test_no_recursive_pep593_from_pep695(
-        self, decl_base: Type[DeclarativeBase]
+        self, decl_base: Type[DeclarativeBase], pep_593_types
     ):
+
         def declare():
             class MyClass(decl_base):
                 __tablename__ = "my_table"
@@ -1791,7 +1823,11 @@ class Pep593InterpretationTests(fixtures.TestBase, testing.AssertsCompiledSQL):
     @testing.variation("alias_type", ["plain", "pep695"])
     @testing.requires.python312
     def test_generic_typealias_pep593(
-        self, decl_base: Type[DeclarativeBase], alias_type: Variation, in_map
+        self,
+        decl_base: Type[DeclarativeBase],
+        alias_type: Variation,
+        in_map,
+        pep_593_types,
     ):
 
         if in_map:
@@ -2158,6 +2194,18 @@ class TypeResolutionTests(fixtures.TestBase, testing.AssertsCompiledSQL):
     def test_plain_typealias_as_typemap_keys(
         self, decl_base: Type[DeclarativeBase]
     ):
+
+        # anno only: global _StrTypeAlias, _UnionTypeAlias
+
+        class _SomeDict1(TypedDict):
+            type: Literal["1"]
+
+        class _SomeDict2(TypedDict):
+            type: Literal["2"]
+
+        _StrTypeAlias = str
+        _UnionTypeAlias = Union[_SomeDict1, _SomeDict2]
+
         decl_base.registry.update_type_annotation_map(
             {_UnionTypeAlias: JSON, _StrTypeAlias: String(30)}
         )
@@ -2327,7 +2375,7 @@ class TypeResolutionTests(fixtures.TestBase, testing.AssertsCompiledSQL):
     )
     @testing.requires.python312
     def test_pep695_typealias_as_typemap_keys(
-        self, decl_base: Type[DeclarativeBase], type_
+        self, decl_base: Type[DeclarativeBase], type_, pep_695_types
     ):
         """test #10807, #12829"""
 
@@ -2525,6 +2573,20 @@ class TypeResolutionTests(fixtures.TestBase, testing.AssertsCompiledSQL):
     def test_optional_in_annotation_map(self, union):
         """See issue #11370"""
 
+        global _Json, _JsonPep604, _JsonPep695
+
+        _JsonPrimitive = Union[str, int, float, bool, None]
+        _JsonObject = Dict[str, "_Json"]
+        _JsonArray = List["_Json"]
+        _Json = Union[_JsonObject, _JsonArray, _JsonPrimitive]
+        _JsonPrimitivePep604 = str | int | float | bool | None
+        _JsonObjectPep604 = dict[str, "_JsonPep604"]
+        _JsonArrayPep604 = list["_JsonPep604"]
+        _JsonPep604 = (
+            _JsonObjectPep604 | _JsonArrayPep604 | _JsonPrimitivePep604
+        )
+        _JsonPep695 = TypeAliasType("_JsonPep695", _JsonPep604)
+
         class Base(DeclarativeBase):
             if union.union:
                 type_annotation_map = {_Json: JSON}
@@ -2862,7 +2924,9 @@ class ResolveToEnumTest(fixtures.TestBase, testing.AssertsCompiledSQL):
     )
     @testing.combinations(True, False, argnames="in_map")
     @testing.requires.python312
-    def test_pep695_literal_defaults_to_enum(self, decl_base, type_, in_map):
+    def test_pep695_literal_defaults_to_enum(
+        self, decl_base, type_, in_map, pep_695_types
+    ):
         """test #11305."""
 
         def declare():