--- /dev/null
+.. change::
+ :tags: bug, tests
+ :tickets: 12668
+
+ Backported to SQLAlchemy 1.4 an improvement to the test suite with regards
+ to how asyncio related tests are run, now using the newer Python 3.11
+ ``asyncio.Runner`` or a backported equivalent, rather than relying on the
+ previous implementation based on ``asyncio.get_event_loop()``. This allows
+ the SQLAlchemy 1.4 codebase to run on Python 3.14 which has removed this
+ method. Pull request courtesy Nils Philippsen.
+
import inspect
from . import config
-from ..util.concurrency import _util_async_run
-from ..util.concurrency import _util_async_run_coroutine_function
+from ..util.concurrency import _AsyncUtil
# may be set to False if the
# --disable-asyncio flag is passed to the test runner.
ENABLE_ASYNCIO = True
+_async_util = _AsyncUtil() # it has lazy init so just always create one
+
+
+def _shutdown():
+ """called when the test finishes"""
+ _async_util.close()
def _run_coroutine_function(fn, *args, **kwargs):
- return _util_async_run_coroutine_function(fn, *args, **kwargs)
+ return _async_util.run(fn, *args, **kwargs)
def _assume_async(fn, *args, **kwargs):
if not ENABLE_ASYNCIO:
return fn(*args, **kwargs)
- return _util_async_run(fn, *args, **kwargs)
+ return _async_util.run_in_greenlet(fn, *args, **kwargs)
def _maybe_async_provisioning(fn, *args, **kwargs):
return fn(*args, **kwargs)
if config.any_async:
- return _util_async_run(fn, *args, **kwargs)
+ return _async_util.run_in_greenlet(fn, *args, **kwargs)
else:
return fn(*args, **kwargs)
is_async = config._current.is_async
if is_async:
- return _util_async_run(fn, *args, **kwargs)
+ return _async_util.run_in_greenlet(fn, *args, **kwargs)
else:
return fn(*args, **kwargs)
collect_types.dump_stats(session.config.option.dump_pyannotate)
+def pytest_unconfigure(config):
+ from sqlalchemy.testing import asyncio
+
+ asyncio._shutdown()
+
+
def pytest_collection_finish(session):
if session.config.option.dump_pyannotate:
from pyannotate_runtime import collect_types
lambda: util.py38, "Python 3.8 or above required"
)
+ @property
+ def not_python314(self):
+ """This requirement is interim to assist with backporting of
+ issue #12405.
+
+ SQLAlchemy 1.4 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(
from .compat import py311
from .compat import py312
from .compat import py313
+from .compat import py314
from .compat import py37
from .compat import py38
from .compat import py39
from typing import Any
from typing import Callable
from typing import Coroutine
+from typing import TypeVar
+from typing import Union
import greenlet
from . import compat
from .langhelpers import memoized_property
from .. import exc
+from ..util import py311
+
+_T = TypeVar("_T")
# If greenlet.gr_context is present in current version of greenlet,
# it will be set with the current context on creation.
self.mutex.release()
-def _util_async_run_coroutine_function(fn, *args, **kwargs):
- """for test suite/ util only"""
-
- loop = get_event_loop()
- if loop.is_running():
- raise Exception(
- "for async run coroutine we expect that no greenlet or event "
- "loop is running when we start out"
- )
- return loop.run_until_complete(fn(*args, **kwargs))
-
-
-def _util_async_run(fn, *args, **kwargs):
- """for test suite/ util only"""
-
- loop = get_event_loop()
- if not loop.is_running():
- return loop.run_until_complete(greenlet_spawn(fn, *args, **kwargs))
- else:
- # allow for a wrapped test function to call another
- assert getattr(
- greenlet.getcurrent(), "__sqlalchemy_greenlet_provider__", False
- )
- return fn(*args, **kwargs)
-
-
def get_event_loop():
"""vendor asyncio.get_event_loop() for python 3.7 and above.
return asyncio.get_event_loop_policy().get_event_loop()
else:
return asyncio.get_event_loop()
+
+
+if py311:
+ _Runner = asyncio.Runner
+else:
+
+ class _Runner: # type: ignore[no-redef]
+ """Runner implementation for test only"""
+
+ _loop: Union[None, asyncio.AbstractEventLoop, bool]
+
+ def __init__(self) -> None:
+ self._loop = None
+
+ def __enter__(self):
+ self._lazy_init()
+ return self
+
+ def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
+ self.close()
+
+ def close(self) -> None:
+ if self._loop:
+ try:
+ self._loop.run_until_complete(
+ self._loop.shutdown_asyncgens()
+ )
+ finally:
+ self._loop.close()
+ self._loop = False
+
+ def get_loop(self) -> asyncio.AbstractEventLoop:
+ """Return embedded event loop."""
+ self._lazy_init()
+ assert self._loop
+ return self._loop
+
+ def run(self, coro: Coroutine[Any, Any, _T]) -> _T:
+ self._lazy_init()
+ assert self._loop
+ return self._loop.run_until_complete(coro)
+
+ def _lazy_init(self) -> None:
+ if self._loop is False:
+ raise RuntimeError("Runner is closed")
+ if self._loop is None:
+ self._loop = asyncio.new_event_loop()
import platform
import sys
+py314 = sys.version_info >= (3, 14)
py313 = sys.version_info >= (3, 13)
py312 = sys.version_info >= (3, 12)
py311 = sys.version_info >= (3, 11)
from ._concurrency_py3k import greenlet_spawn
from ._concurrency_py3k import is_exit_exception
from ._concurrency_py3k import AsyncAdaptedLock
- from ._concurrency_py3k import _util_async_run # noqa: F401
- from ._concurrency_py3k import (
- _util_async_run_coroutine_function,
- ) # noqa: F401, E501
+ from ._concurrency_py3k import _Runner
from ._concurrency_py3k import asyncio # noqa: F401
- # does not need greennlet, just Python 3
+ # does not need greenlet, just Python 3
from ._compat_py3k import asynccontextmanager # noqa: F401
+
+class _AsyncUtil:
+ """Asyncio util for test suite/ util only"""
+
+ def __init__(self):
+ if have_greenlet:
+ self.runner = _Runner()
+
+ def run(self, fn, *args, **kwargs):
+ """Run coroutine on the loop"""
+ return self.runner.run(fn(*args, **kwargs))
+
+ def run_in_greenlet(self, fn, *args, **kwargs):
+ """Run sync function in greenlet. Support nested calls"""
+ if have_greenlet:
+ if self.runner.get_loop().is_running():
+ return fn(*args, **kwargs)
+ else:
+ return self.runner.run(greenlet_spawn(fn, *args, **kwargs))
+ else:
+ return fn(*args, **kwargs)
+
+ def close(self):
+ if have_greenlet:
+ self.runner.close()
+
+
if not have_greenlet:
asyncio = None # noqa: F811
Programming Language :: Python :: 3.10
Programming Language :: Python :: 3.11
Programming Language :: Python :: 3.12
+ Programming Language :: Python :: 3.13
+ Programming Language :: Python :: 3.14
Programming Language :: Python :: Implementation :: CPython
Programming Language :: Python :: Implementation :: PyPy
Topic :: Database :: Front-Ends
A003, A004, A005, A006
D,
E203,E305,E711,E712,E721,E722,E741,
- FA100,
+ FA100,F824
N801,N802,N806,
RST304,RST303,RST299,RST399,
W503,W504
# 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()
with expect_raises_message(ValueError, "sync error"):
await greenlet_spawn(go)
+ @testing.requires.not_python314
def test_await_fallback_no_greenlet(self):
to_await = run1()
await_fallback(to_await)
cov: True
extras=
- py{3,38,39,310,311,312}: {[greenletextras]extras}
+ py{3,38,39,310,311,312,313,314}: {[greenletextras]extras}
postgresql: postgresql
postgresql: postgresql-pg8000
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
dbapimain-mysql: git+https://github.com/PyMySQL/mysqlclient-python.git#egg=mysqlclient
dbapimain-mysql: git+https://github.com/PyMySQL/PyMySQL.git#egg=pymysql
- # dbapimain-mysql: git+https://github.com/mariadb-corporation/mariadb-connector-python#egg=mariadb
dbapimain-oracle: git+https://github.com/oracle/python-cx_Oracle.git#egg=cx_Oracle
- py313-mssql: git+https://github.com/mkleehammer/pyodbc.git\#egg=pyodbc
dbapimain-mssql: git+https://github.com/mkleehammer/pyodbc.git\#egg=pyodbc
cov: pytest-cov
PYTEST_COLOR={tty:--color=yes}
- # pytest 'rewrite' is hitting lots of deprecation warnings under py312 and
- # i can't find any way to ignore those warnings, so this turns it off
- py312: PYTEST_ARGS=--assert plain
-
MEMUSAGE=--nomemory
BASECOMMAND=python -m pytest {env:PYTEST_COLOR} --rootdir {toxinidir} --log-info=sqlalchemy.testing
sqlite-nogreenlet: EXTRA_SQLITE_DRIVERS={env:EXTRA_SQLITE_DRIVERS:--dbdriver sqlite --dbdriver pysqlite_numeric}
- py{37,38,39}-sqlite_file: EXTRA_SQLITE_DRIVERS={env:EXTRA_SQLITE_DRIVERS:--dbdriver sqlite --dbdriver aiosqlite --dbdriver pysqlcipher}
-
- # omit pysqlcipher for Python 3.10
- py3{,10,11}-sqlite_file: EXTRA_SQLITE_DRIVERS={env:EXTRA_SQLITE_DRIVERS:--dbdriver sqlite --dbdriver aiosqlite}
+ py-sqlite_file: EXTRA_SQLITE_DRIVERS={env:EXTRA_SQLITE_DRIVERS:--dbdriver sqlite --dbdriver aiosqlite}
postgresql: POSTGRESQL={env:TOX_POSTGRESQL:--db postgresql}
py2{,7}-postgresql: POSTGRESQL={env:TOX_POSTGRESQL_PY2K:{env:TOX_POSTGRESQL:--db postgresql}}
- py3{,5,6,7,8,9,10,11}-postgresql: EXTRA_PG_DRIVERS={env:EXTRA_PG_DRIVERS:--dbdriver psycopg2 --dbdriver asyncpg --dbdriver pg8000}
+ py3{,5,6,7,8,9,10,11,12,13,14}-postgresql: EXTRA_PG_DRIVERS={env:EXTRA_PG_DRIVERS:--dbdriver psycopg2 --dbdriver asyncpg --dbdriver pg8000}
py312-postgresql: EXTRA_PG_DRIVERS={env:EXTRA_PG_DRIVERS:--dbdriver psycopg2 --dbdriver pg8000}
mysql: MYSQL={env:TOX_MYSQL:--db mysql}