]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
implement pep-649 workarounds, test suite passing for python 3.14
authorMike Bayer <mike_mp@zzzcomputing.com>
Thu, 6 Mar 2025 14:12:43 +0000 (09:12 -0500)
committerMike Bayer <mike_mp@zzzcomputing.com>
Thu, 8 May 2025 22:58:58 +0000 (18:58 -0400)
Changes to the test suite to accommodate Python 3.14 as of version
3.14.0b1

Originally this included a major breaking change to how python 3.14
implemented :pep:`649`, however this was resolved by [1].

As of a7, greenlet is skipped due to issues in a7 and later b1
in [2].

1. the change to rewrite all conditionals in annotation related tests
   is reverted.
2. test_memusage needed an explicit set_start_method() call so that
   it can continue to use plain fork
3. unfortunately at the moment greenlet has to be re-disabled for 3.14.
4. Changes to tox overall, remove pysqlcipher which hasn't worked
   in years, etc.
5. we need to support upcoming typing-extensions also, install the beta
6. 3.14.0a7 introduces major regressions to our runtime typing
   utilities, unfortunately, it's not clear if these can be resolved
7. for 3.14.0b1, we have to vendor get_annotations to work around [3]

[1] https://github.com/python/cpython/issues/130881
[2] https://github.com/python-greenlet/greenlet/issues/440
[3] https://github.com/python/cpython/issues/133684

py314: yes
Fixes: #12405
References: #12399
Change-Id: I8715d02fae599472dd64a2a46ccf8986239ecd99

12 files changed:
doc/build/changelog/unreleased_20/12405.rst [new file with mode: 0644]
lib/sqlalchemy/testing/requirements.py
lib/sqlalchemy/util/__init__.py
lib/sqlalchemy/util/compat.py
lib/sqlalchemy/util/langhelpers.py
lib/sqlalchemy/util/typing.py
pyproject.toml
test/aaa_profiling/test_memusage.py
test/base/test_typing_utils.py
test/ext/asyncio/test_engine_py3k.py
test/typing/test_overloads.py
tox.ini

diff --git a/doc/build/changelog/unreleased_20/12405.rst b/doc/build/changelog/unreleased_20/12405.rst
new file mode 100644 (file)
index 0000000..f90546a
--- /dev/null
@@ -0,0 +1,10 @@
+.. change::
+    :tags: bug, orm
+    :tickets: 12405
+
+    Changes to the test suite to accommodate Python 3.14 and its new
+    implementation of :pep:`649`, which highly modifies how typing annotations
+    are interpreted at runtime.  Use of the new
+    ``annotationlib.get_annotations()`` function is enabled when python 3.14 is
+    present, and many other changes to how pep-484 type objects are interpreted
+    at runtime are made.
index 7c4d2fb605b4184ef287d06ca254a2c2b9a597eb..f0384eb91af0c4f653d198364bbdb81cb6412408 100644 (file)
@@ -19,6 +19,7 @@ to provide specific inclusion/exclusions.
 
 from __future__ import annotations
 
+import os
 import platform
 
 from . import asyncio as _test_asyncio
@@ -1498,6 +1499,10 @@ class SuiteRequirements(Requirements):
 
         return config.add_to_marker.timing_intensive
 
+    @property
+    def posix(self):
+        return exclusions.skip_if(lambda: os.name != "posix")
+
     @property
     def memory_intensive(self):
         from . import config
@@ -1539,6 +1544,27 @@ class SuiteRequirements(Requirements):
 
         return exclusions.skip_if(check)
 
+    @property
+    def up_to_date_typealias_type(self):
+        # this checks a particular quirk found in typing_extensions <=4.12.0
+        # using older python versions like 3.10 or 3.9, we use TypeAliasType
+        # from typing_extensions which does not provide for sufficient
+        # introspection prior to 4.13.0
+        def check(config):
+            import typing
+            import typing_extensions
+
+            TypeAliasType = getattr(
+                typing, "TypeAliasType", typing_extensions.TypeAliasType
+            )
+            TV = typing.TypeVar("TV")
+            TA_generic = TypeAliasType(  # type: ignore
+                "TA_generic", typing.List[TV], type_params=(TV,)
+            )
+            return hasattr(TA_generic[int], "__value__")
+
+        return exclusions.only_if(check)
+
     @property
     def python310(self):
         return exclusions.only_if(
@@ -1557,6 +1583,26 @@ class SuiteRequirements(Requirements):
             lambda: util.py312, "Python 3.12 or above required"
         )
 
+    @property
+    def fail_python314b1(self):
+        return exclusions.fails_if(
+            lambda: util.compat.py314b1, "Fails as of python 3.14.0b1"
+        )
+
+    @property
+    def not_python314(self):
+        """This requirement is interim to assist with backporting of
+        issue #12405.
+
+        SQLAlchemy 2.0 still includes the ``await_fallback()`` method that
+        makes use of ``asyncio.get_event_loop_policy()``.  This is removed
+        in SQLAlchemy 2.1.
+
+        """
+        return exclusions.skip_if(
+            lambda: util.py314, "Python 3.14 or above not supported"
+        )
+
     @property
     def cpython(self):
         return exclusions.only_if(
index 73ee1709cc01a996d09995facae59b919ffbe5ae..0b8170ebb720cefdf4071c5741504b520dceb2c8 100644 (file)
@@ -65,6 +65,7 @@ from .compat import py310 as py310
 from .compat import py311 as py311
 from .compat import py312 as py312
 from .compat import py313 as py313
+from .compat import py314 as py314
 from .compat import pypy as pypy
 from .compat import win32 as win32
 from .concurrency import await_ as await_
index a65de17f5b5f9fd46713b12c6170aabbdc19bf00..7dd777546894e42de84b9597969357ff49adb738 100644 (file)
@@ -31,6 +31,8 @@ from typing import Set
 from typing import Tuple
 from typing import Type
 
+py314b1 = sys.version_info >= (3, 14, 0, "beta", 1)
+py314 = sys.version_info >= (3, 14)
 py313 = sys.version_info >= (3, 13)
 py312 = sys.version_info >= (3, 12)
 py311 = sys.version_info >= (3, 11)
index 6868c81f5b502366e56b4ec046bb897d87134e22..666b059eed1e01d36341e529bdf693529c1cd882 100644 (file)
@@ -58,7 +58,85 @@ _F = TypeVar("_F", bound=Callable[..., Any])
 _MA = TypeVar("_MA", bound="HasMemoized.memoized_attribute[Any]")
 _M = TypeVar("_M", bound=ModuleType)
 
-if compat.py310:
+if compat.py314:
+    # vendor a minimal form of get_annotations per
+    # https://github.com/python/cpython/issues/133684#issuecomment-2863841891
+
+    from annotationlib import call_annotate_function  # type: ignore
+    from annotationlib import Format
+
+    def _get_and_call_annotate(obj, format):  # noqa: A002
+        annotate = getattr(obj, "__annotate__", None)
+        if annotate is not None:
+            ann = call_annotate_function(annotate, format, owner=obj)
+            if not isinstance(ann, dict):
+                raise ValueError(f"{obj!r}.__annotate__ returned a non-dict")
+            return ann
+        return None
+
+    # this is ported from py3.13.0a7
+    _BASE_GET_ANNOTATIONS = type.__dict__["__annotations__"].__get__  # type: ignore  # noqa: E501
+
+    def _get_dunder_annotations(obj):
+        if isinstance(obj, type):
+            try:
+                ann = _BASE_GET_ANNOTATIONS(obj)
+            except AttributeError:
+                # For static types, the descriptor raises AttributeError.
+                return {}
+        else:
+            ann = getattr(obj, "__annotations__", None)
+            if ann is None:
+                return {}
+
+        if not isinstance(ann, dict):
+            raise ValueError(
+                f"{obj!r}.__annotations__ is neither a dict nor None"
+            )
+        return dict(ann)
+
+    def _vendored_get_annotations(
+        obj: Any, *, format: Format  # noqa: A002
+    ) -> Mapping[str, Any]:
+        """A sparse implementation of annotationlib.get_annotations()"""
+
+        try:
+            ann = _get_dunder_annotations(obj)
+        except Exception:
+            pass
+        else:
+            if ann is not None:
+                return dict(ann)
+
+        # But if __annotations__ threw a NameError, we try calling __annotate__
+        ann = _get_and_call_annotate(obj, format)
+        if ann is None:
+            # If that didn't work either, we have a very weird object:
+            # evaluating
+            # __annotations__ threw NameError and there is no __annotate__.
+            # In that case,
+            # we fall back to trying __annotations__ again.
+            ann = _get_dunder_annotations(obj)
+
+        if ann is None:
+            if isinstance(obj, type) or callable(obj):
+                return {}
+            raise TypeError(f"{obj!r} does not have annotations")
+
+        if not ann:
+            return {}
+
+        return dict(ann)
+
+    def get_annotations(obj: Any) -> Mapping[str, Any]:
+        # FORWARDREF has the effect of giving us ForwardRefs and not
+        # actually trying to evaluate the annotations.  We need this so
+        # that the annotations act as much like
+        # "from __future__ import annotations" as possible, which is going
+        # away in future python as a separate mode
+        return _vendored_get_annotations(obj, format=Format.FORWARDREF)
+
+elif compat.py310:
 
     def get_annotations(obj: Any) -> Mapping[str, Any]:
         return inspect.get_annotations(obj)
index c356b4912668c5d53993961eba98586be1ae0bdc..7a59dd536ee1d650d64d8b99b38f4081cb02f8ea 100644 (file)
@@ -77,7 +77,9 @@ if compat.py310:
 else:
     NoneType = type(None)  # type: ignore
 
-NoneFwd = ForwardRef("None")
+
+def is_fwd_none(typ: Any) -> bool:
+    return isinstance(typ, ForwardRef) and typ.__forward_arg__ == "None"
 
 
 _AnnotationScanType = Union[
@@ -393,7 +395,7 @@ def pep695_values(type_: _AnnotationScanType) -> Set[Any]:
             if isinstance(t, list):
                 stack.extend(t)
             else:
-                types.add(None if t in {NoneType, NoneFwd} else t)
+                types.add(None if t is NoneType or is_fwd_none(t) else t)
         return types
     else:
         return {res}
@@ -445,10 +447,11 @@ def de_optionalize_union_types(
         return _de_optionalize_fwd_ref_union_types(type_, False)
 
     elif is_union(type_) and includes_none(type_):
-        typ = set(type_.__args__)
-
-        typ.discard(NoneType)
-        typ.discard(NoneFwd)
+        typ = {
+            t
+            for t in type_.__args__
+            if t is not NoneType and not is_fwd_none(t)
+        }
 
         return make_union_type(*typ)
 
@@ -524,7 +527,8 @@ def _de_optionalize_fwd_ref_union_types(
 
 def make_union_type(*types: _AnnotationScanType) -> Type[Any]:
     """Make a Union type."""
-    return Union.__getitem__(types)  # type: ignore
+
+    return Union[types]  # type: ignore
 
 
 def includes_none(type_: Any) -> bool:
@@ -550,7 +554,7 @@ def includes_none(type_: Any) -> bool:
     if is_newtype(type_):
         return includes_none(type_.__supertype__)
     try:
-        return type_ in (NoneFwd, NoneType, None)
+        return type_ in (NoneType, None) or is_fwd_none(type_)
     except TypeError:
         # if type_ is Column, mapped_column(), etc. the use of "in"
         # resolves to ``__eq__()`` which then gives us an expression object
index f3704cab21bf6767caa8dab5684c479f01c733bc..4365a9a7f08d49dc19dccc7db0ace6b7abe0e3fe 100644 (file)
@@ -154,6 +154,13 @@ filterwarnings = [
     # sqlite3 warnings due to test/dialect/test_sqlite.py->test_native_datetime,
     # which is asserting that these deprecated-in-py312 handlers are functional
     "ignore:The default (date)?(time)?(stamp)? (adapter|converter):DeprecationWarning",
+
+    # warning regarding using "fork" mode for multiprocessing when the parent
+    # has threads; using pytest-xdist introduces threads in the parent
+    # and we use multiprocessing in test/aaa_profiling/test_memusage.py where
+    # we require "fork" mode
+    # https://github.com/python/cpython/pull/100229#issuecomment-2704616288
+    "ignore:This process .* is multi-threaded:DeprecationWarning",
 ]
 markers = [
     "memory_intensive: memory / CPU intensive suite tests",
index 01c1134538e1afa8c57367c94b6e8c5fd28968b0..d3e7dfb7c0e1ac0e3e255753870ba2af1a1a5bdf 100644 (file)
@@ -223,10 +223,14 @@ def profile_memory(
         # return run_plain
 
         def run_in_process(*func_args):
-            queue = multiprocessing.Queue()
-            proc = multiprocessing.Process(
-                target=profile, args=(queue, func_args)
-            )
+            # see
+            # https://docs.python.org/3.14/whatsnew/3.14.html
+            # #incompatible-changes - the default run type is no longer
+            # "fork", but since we are running closures in the process
+            # we need forked mode
+            ctx = multiprocessing.get_context("fork")
+            queue = ctx.Queue()
+            proc = ctx.Process(target=profile, args=(queue, func_args))
             proc.start()
             while True:
                 row = queue.get()
@@ -394,7 +398,7 @@ class MemUsageTest(EnsureZeroed):
 
 @testing.add_to_marker.memory_intensive
 class MemUsageWBackendTest(fixtures.MappedTest, EnsureZeroed):
-    __requires__ = "cpython", "memory_process_intensive", "no_asyncio"
+    __requires__ = "cpython", "posix", "memory_process_intensive", "no_asyncio"
     __sparse_backend__ = True
 
     # ensure a pure growing test trips the assertion
index 7a6aca3c857b2ac033c5a8fccd2ad223aff4455a..b1ba3cdee105eb7df25c4b261f976e0ca5eb1945 100644 (file)
@@ -10,8 +10,8 @@ from sqlalchemy.testing import requires
 from sqlalchemy.testing.assertions import eq_
 from sqlalchemy.testing.assertions import is_
 from sqlalchemy.util import py310
-from sqlalchemy.util import py311
 from sqlalchemy.util import py312
+from sqlalchemy.util import py314
 from sqlalchemy.util import typing as sa_typing
 
 TV = typing.TypeVar("TV")
@@ -39,9 +39,10 @@ def null_union_types():
 
 
 def generic_unions():
-    # remove new-style unions `int | str` that are not generic
     res = union_types() + null_union_types()
-    if py310:
+    if py310 and not py314:
+        # for py310 through py313, remove new-style unions `int | str` that
+        # are not generic
         new_ut = type(int | str)
         res = [t for t in res if not isinstance(t, new_ut)]
     return res
@@ -199,6 +200,29 @@ A_null_union = typing_extensions.Annotated[
 ]
 
 
+def compare_type_by_string(a, b):
+    """python 3.14 has made ForwardRefs not really comparable or reliably
+    hashable.
+
+    As we need to compare types here, including structures like
+    `Union["str", "int"]`, without having to dive into cpython's source code
+    each time a new release comes out, compare based on stringification,
+    which still presents changing rules but at least are easy to diagnose
+    and correct for different python versions.
+
+    See discussion at https://github.com/python/cpython/issues/129463
+    for background
+
+    """
+
+    if isinstance(a, (set, list)):
+        a = sorted(a, key=lambda x: str(x))
+    if isinstance(b, (set, list)):
+        b = sorted(b, key=lambda x: str(x))
+
+    eq_(str(a), str(b))
+
+
 def annotated_l():
     return [A_str, A_null_str, A_union, A_null_union]
 
@@ -233,14 +257,6 @@ class TestTestingThings(fixtures.TestBase):
         is_(typing.Union, typing_extensions.Union)
         is_(typing.Optional, typing_extensions.Optional)
 
-    def test_make_union(self):
-        v = int, str
-        eq_(typing.Union[int, str], typing.Union.__getitem__(v))
-        if py311:
-            # need eval since it's a syntax error in python < 3.11
-            eq_(typing.Union[int, str], eval("typing.Union[*(int, str)]"))
-            eq_(typing.Union[int, str], eval("typing.Union[*v]"))
-
     @requires.python312
     def test_make_type_alias_type(self):
         # verify that TypeAliasType('foo', int) it the same as 'type foo = int'
@@ -252,9 +268,11 @@ class TestTestingThings(fixtures.TestBase):
         eq_(x_type.__value__, x.__value__)
 
     def test_make_fw_ref(self):
-        eq_(make_fw_ref("str"), typing.ForwardRef("str"))
-        eq_(make_fw_ref("str|int"), typing.ForwardRef("str|int"))
-        eq_(
+        compare_type_by_string(make_fw_ref("str"), typing.ForwardRef("str"))
+        compare_type_by_string(
+            make_fw_ref("str|int"), typing.ForwardRef("str|int")
+        )
+        compare_type_by_string(
             make_fw_ref("Optional[Union[str, int]]"),
             typing.ForwardRef("Optional[Union[str, int]]"),
         )
@@ -315,8 +333,11 @@ class TestTyping(fixtures.TestBase):
         ]
 
         for t in all_types():
-            # use is since union compare equal between new/old style
-            exp = any(t is k for k in generics)
+            if py314:
+                exp = any(t == k for k in generics)
+            else:
+                # use is since union compare equal between new/old style
+                exp = any(t is k for k in generics)
             eq_(sa_typing.is_generic(t), exp, t)
 
     def test_is_pep695(self):
@@ -357,70 +378,82 @@ class TestTyping(fixtures.TestBase):
         eq_(sa_typing.pep695_values(TAext_null_union), {int, str, None})
         eq_(sa_typing.pep695_values(TA_null_union2), {int, str, None})
         eq_(sa_typing.pep695_values(TAext_null_union2), {int, str, None})
-        eq_(
+
+        compare_type_by_string(
             sa_typing.pep695_values(TA_null_union3),
-            {int, typing.ForwardRef("typing.Union[None, bool]")},
+            [int, typing.ForwardRef("typing.Union[None, bool]")],
         )
-        eq_(
+
+        compare_type_by_string(
             sa_typing.pep695_values(TAext_null_union3),
             {int, typing.ForwardRef("typing.Union[None, bool]")},
         )
-        eq_(
+
+        compare_type_by_string(
             sa_typing.pep695_values(TA_null_union4),
-            {int, typing.ForwardRef("TA_null_union2")},
+            [int, typing.ForwardRef("TA_null_union2")],
         )
-        eq_(
+        compare_type_by_string(
             sa_typing.pep695_values(TAext_null_union4),
             {int, typing.ForwardRef("TAext_null_union2")},
         )
+
         eq_(sa_typing.pep695_values(TA_union_ta), {int, str})
         eq_(sa_typing.pep695_values(TAext_union_ta), {int, str})
         eq_(sa_typing.pep695_values(TA_null_union_ta), {int, str, None, float})
-        eq_(
+
+        compare_type_by_string(
             sa_typing.pep695_values(TAext_null_union_ta),
             {int, str, None, float},
         )
-        eq_(
+
+        compare_type_by_string(
             sa_typing.pep695_values(TA_list),
-            {int, str, typing.List[typing.ForwardRef("TA_list")]},
+            [int, str, typing.List[typing.ForwardRef("TA_list")]],
         )
-        eq_(
+
+        compare_type_by_string(
             sa_typing.pep695_values(TAext_list),
             {int, str, typing.List[typing.ForwardRef("TAext_list")]},
         )
-        eq_(
+
+        compare_type_by_string(
             sa_typing.pep695_values(TA_recursive),
-            {typing.ForwardRef("TA_recursive"), str},
+            [str, typing.ForwardRef("TA_recursive")],
         )
-        eq_(
+        compare_type_by_string(
             sa_typing.pep695_values(TAext_recursive),
             {typing.ForwardRef("TAext_recursive"), str},
         )
-        eq_(
+        compare_type_by_string(
             sa_typing.pep695_values(TA_null_recursive),
-            {typing.ForwardRef("TA_recursive"), str, None},
+            [str, typing.ForwardRef("TA_recursive"), None],
         )
-        eq_(
+        compare_type_by_string(
             sa_typing.pep695_values(TAext_null_recursive),
             {typing.ForwardRef("TAext_recursive"), str, None},
         )
-        eq_(
+        compare_type_by_string(
             sa_typing.pep695_values(TA_recursive_a),
-            {typing.ForwardRef("TA_recursive_b"), int},
+            [int, typing.ForwardRef("TA_recursive_b")],
         )
-        eq_(
+        compare_type_by_string(
             sa_typing.pep695_values(TAext_recursive_a),
             {typing.ForwardRef("TAext_recursive_b"), int},
         )
-        eq_(
+        compare_type_by_string(
             sa_typing.pep695_values(TA_recursive_b),
-            {typing.ForwardRef("TA_recursive_a"), str},
+            [str, typing.ForwardRef("TA_recursive_a")],
         )
-        eq_(
+        compare_type_by_string(
             sa_typing.pep695_values(TAext_recursive_b),
             {typing.ForwardRef("TAext_recursive_a"), str},
         )
+
+    @requires.up_to_date_typealias_type
+    def test_pep695_value_generics(self):
         # generics
+
         eq_(sa_typing.pep695_values(TA_generic), {typing.List[TV]})
         eq_(sa_typing.pep695_values(TAext_generic), {typing.List[TV]})
         eq_(sa_typing.pep695_values(TA_generic_typed), {typing.List[TV]})
@@ -456,17 +489,23 @@ class TestTyping(fixtures.TestBase):
             fn(typing.Optional[typing.Union[int, str]]), typing.Union[int, str]
         )
         eq_(fn(typing.Union[int, str, None]), typing.Union[int, str])
+
         eq_(fn(typing.Union[int, str, "None"]), typing.Union[int, str])
 
         eq_(fn(make_fw_ref("None")), typing_extensions.Never)
         eq_(fn(make_fw_ref("typing.Union[None]")), typing_extensions.Never)
         eq_(fn(make_fw_ref("Union[None, str]")), typing.ForwardRef("str"))
-        eq_(
+
+        compare_type_by_string(
             fn(make_fw_ref("Union[None, str, int]")),
             typing.Union["str", "int"],
         )
-        eq_(fn(make_fw_ref("Optional[int]")), typing.ForwardRef("int"))
-        eq_(
+
+        compare_type_by_string(
+            fn(make_fw_ref("Optional[int]")), typing.ForwardRef("int")
+        )
+
+        compare_type_by_string(
             fn(make_fw_ref("typing.Optional[Union[int | str]]")),
             typing.ForwardRef("Union[int | str]"),
         )
@@ -479,9 +518,12 @@ class TestTyping(fixtures.TestBase):
         for t in union_types() + type_aliases() + new_types() + annotated_l():
             eq_(fn(t), t)
 
-        eq_(
+        compare_type_by_string(
             fn(make_fw_ref("Union[typing.Dict[str, int], int, None]")),
-            typing.Union["typing.Dict[str, int]", "int"],
+            typing.Union[
+                "typing.Dict[str, int]",
+                "int",
+            ],
         )
 
     def test_make_union_type(self):
@@ -505,22 +547,14 @@ class TestTyping(fixtures.TestBase):
             typing.Union[bool, TAext_int, NT_str],
         )
 
-    def test_includes_none(self):
-        eq_(sa_typing.includes_none(None), True)
-        eq_(sa_typing.includes_none(type(None)), True)
-        eq_(sa_typing.includes_none(typing.ForwardRef("None")), True)
-        eq_(sa_typing.includes_none(int), False)
-        for t in union_types():
-            eq_(sa_typing.includes_none(t), False)
-
-        for t in null_union_types():
-            eq_(sa_typing.includes_none(t), True, str(t))
-
+    @requires.up_to_date_typealias_type
+    def test_includes_none_generics(self):
         # TODO: these are false negatives
         false_negatives = {
             TA_null_union4,  # does not evaluate FW ref
             TAext_null_union4,  # does not evaluate FW ref
         }
+
         for t in type_aliases() + new_types():
             if t in false_negatives:
                 exp = False
@@ -528,6 +562,17 @@ class TestTyping(fixtures.TestBase):
                 exp = "null" in t.__name__
             eq_(sa_typing.includes_none(t), exp, str(t))
 
+    def test_includes_none(self):
+        eq_(sa_typing.includes_none(None), True)
+        eq_(sa_typing.includes_none(type(None)), True)
+        eq_(sa_typing.includes_none(typing.ForwardRef("None")), True)
+        eq_(sa_typing.includes_none(int), False)
+        for t in union_types():
+            eq_(sa_typing.includes_none(t), False)
+
+        for t in null_union_types():
+            eq_(sa_typing.includes_none(t), True, str(t))
+
         for t in annotated_l():
             eq_(
                 sa_typing.includes_none(t),
index e040aeca114ef9076d599897e5457ca50d633f2f..48226aa27bd577fb50d6953aa0e49004079a5402 100644 (file)
@@ -269,9 +269,16 @@ class AsyncEngineTest(EngineFixture):
 
         is_false(async_engine == None)
 
-    @async_test
-    async def test_no_attach_to_event_loop(self, testing_engine):
-        """test #6409"""
+    def test_no_attach_to_event_loop(self, testing_engine):
+        """test #6409
+
+        note this test does not seem to trigger the bug that was originally
+        fixed in #6409, when using python 3.10 and higher (the original issue
+        can repro in 3.8 at least, based on my testing).  It's been simplified
+        to no longer explicitly create a new loop, asyncio.run() already
+        creates a new loop.
+
+        """
 
         import asyncio
         import threading
@@ -279,9 +286,6 @@ class AsyncEngineTest(EngineFixture):
         errs = []
 
         def go():
-            loop = asyncio.new_event_loop()
-            asyncio.set_event_loop(loop)
-
             async def main():
                 tasks = [task() for _ in range(2)]
 
index 1c50845493cd5597f57232059356f6ff9a632ecb..355b4b568b03a78223a1075c8c195b9c1df60312 100644 (file)
@@ -9,6 +9,7 @@ from sqlalchemy.orm.query import Query
 from sqlalchemy.sql.base import Executable
 from sqlalchemy.testing import fixtures
 from sqlalchemy.testing.assertions import eq_
+from sqlalchemy.util.typing import is_fwd_ref
 
 engine_execution_options = {
     "compiled_cache": "Optional[CompiledCacheType]",
@@ -79,7 +80,12 @@ class OverloadTest(fixtures.TestBase):
 
     @testing.combinations(
         (CoreExecuteOptionsParameter, core_execution_options),
-        (OrmExecuteOptionsParameter, orm_execution_options),
+        # https://github.com/python/cpython/issues/133701
+        (
+            OrmExecuteOptionsParameter,
+            orm_execution_options,
+            testing.requires.fail_python314b1,
+        ),
     )
     def test_typed_dicts(self, typ, expected):
         # we currently expect these to be union types with first entry
@@ -91,7 +97,7 @@ class OverloadTest(fixtures.TestBase):
         expected.pop("opt")
 
         assert_annotations = {
-            key: fwd_ref.__forward_arg__
+            key: fwd_ref.__forward_arg__ if is_fwd_ref(fwd_ref) else fwd_ref
             for key, fwd_ref in typed_dict.__annotations__.items()
         }
         eq_(assert_annotations, expected)
diff --git a/tox.ini b/tox.ini
index caadcedb5e9ec845ccb0dd5834ae2ee0c56a6aef..cf0e9d2bd779d5b0ca5d45f53a8094c24a5294e1 100644 (file)
--- a/tox.ini
+++ b/tox.ini
@@ -28,9 +28,11 @@ usedevelop=
      cov: True
 
 extras=
-     py{3,39,310,311,312,313}: {[greenletextras]extras}
+     # this can be limited to specific python versions IF there is no
+     # greenlet available for the most recent python.  otherwise
+     # keep this present in all cases
+     py{38,39,310,311,312,313}: {[greenletextras]extras}
 
-     py{39,310}-sqlite_file: sqlcipher
      postgresql: postgresql
      postgresql: postgresql_pg8000
      postgresql: postgresql_psycopg
@@ -50,14 +52,13 @@ install_command=
      python -I -m pip install --only-binary=pymssql {opts} {packages}
 
 deps=
+     typing-extensions>=4.13.0rc1
+
      pytest>=7.0.0,<8.4
      # tracked by https://github.com/pytest-dev/pytest-xdist/issues/907
      pytest-xdist!=3.3.0
 
-     py313: git+https://github.com/python-greenlet/greenlet.git\#egg=greenlet
-
      dbapimain-sqlite: git+https://github.com/omnilib/aiosqlite.git\#egg=aiosqlite
-     dbapimain-sqlite: git+https://github.com/coleifer/sqlcipher3.git\#egg=sqlcipher3
 
      dbapimain-postgresql: git+https://github.com/psycopg/psycopg2.git\#egg=psycopg2
      dbapimain-postgresql: git+https://github.com/MagicStack/asyncpg.git\#egg=asyncpg
@@ -115,20 +116,19 @@ setenv=
     oracle: ORACLE={env:TOX_ORACLE:--db oracle}
 
     oracle: EXTRA_ORACLE_DRIVERS={env:EXTRA_ORACLE_DRIVERS:--dbdriver cx_oracle --dbdriver oracledb --dbdriver oracledb_async}
-    py{313,314}-oracle: EXTRA_ORACLE_DRIVERS={env:EXTRA_ORACLE_DRIVERS:--dbdriver cx_oracle --dbdriver oracledb}
 
     sqlite: SQLITE={env:TOX_SQLITE:--db sqlite}
     sqlite_file: SQLITE={env:TOX_SQLITE_FILE:--db sqlite_file}
 
-    sqlite: EXTRA_SQLITE_DRIVERS={env:EXTRA_SQLITE_DRIVERS:--dbdriver sqlite --dbdriver pysqlite_numeric --dbdriver aiosqlite}
-    py{313,314}-sqlite: EXTRA_SQLITE_DRIVERS={env:EXTRA_SQLITE_DRIVERS:--dbdriver sqlite --dbdriver pysqlite_numeric}
-
+    py{38,39,310,311,312,313}-sqlite: EXTRA_SQLITE_DRIVERS={env:EXTRA_SQLITE_DRIVERS:--dbdriver sqlite --dbdriver pysqlite_numeric --dbdriver aiosqlite}
+    py{314}-sqlite: EXTRA_SQLITE_DRIVERS={env:EXTRA_SQLITE_DRIVERS:--dbdriver sqlite --dbdriver pysqlite_numeric}
     sqlite-nogreenlet: EXTRA_SQLITE_DRIVERS={env:EXTRA_SQLITE_DRIVERS:--dbdriver sqlite --dbdriver pysqlite_numeric}
 
-    py{39}-sqlite_file: EXTRA_SQLITE_DRIVERS={env:EXTRA_SQLITE_DRIVERS:--dbdriver sqlite --dbdriver aiosqlite --dbdriver pysqlcipher}
+    # note all of these would need limiting for py314 if we want tests to run until
+    # greenlet is available.   I just dont see any clean way to do this in tox without writing
+    # all the versions out every time and it's ridiculous
 
-    # omit pysqlcipher for Python 3.10
-    py{3,310,311,312}-sqlite_file: EXTRA_SQLITE_DRIVERS={env:EXTRA_SQLITE_DRIVERS:--dbdriver sqlite --dbdriver aiosqlite}
+    sqlite_file: EXTRA_SQLITE_DRIVERS={env:EXTRA_SQLITE_DRIVERS:--dbdriver sqlite --dbdriver aiosqlite}
 
     postgresql: POSTGRESQL={env:TOX_POSTGRESQL:--db postgresql}
 
@@ -148,10 +148,10 @@ setenv=
     mssql: MSSQL={env:TOX_MSSQL:--db mssql}
 
     mssql: EXTRA_MSSQL_DRIVERS={env:EXTRA_MSSQL_DRIVERS:--dbdriver pyodbc --dbdriver aioodbc --dbdriver pymssql}
-    py{313,314}-mssql: EXTRA_MSSQL_DRIVERS={env:EXTRA_MSSQL_DRIVERS:--dbdriver pyodbc  --dbdriver aioodbc}
+    py{314}-mssql: EXTRA_MSSQL_DRIVERS={env:EXTRA_MSSQL_DRIVERS:--dbdriver pyodbc  --dbdriver aioodbc}
 
     mssql-nogreenlet: EXTRA_MSSQL_DRIVERS={env:EXTRA_MSSQL_DRIVERS:--dbdriver pyodbc --dbdriver pymssql}
-    py{313,314}-mssql-nogreenlet: EXTRA_MSSQL_DRIVERS={env:EXTRA_MSSQL_DRIVERS:--dbdriver pyodbc}
+    py{314}-mssql-nogreenlet: EXTRA_MSSQL_DRIVERS={env:EXTRA_MSSQL_DRIVERS:--dbdriver pyodbc}
 
     oracle,mssql,sqlite_file: IDENTS=--write-idents db_idents.txt