From b6cc8343159fc0a27365e09a3beef06433f3f1b5 Mon Sep 17 00:00:00 2001 From: Denis Laxalde Date: Wed, 17 Aug 2022 15:59:31 +0200 Subject: [PATCH] test: use anyio instead of pytest-asyncio This is in preparation for adding support for other async libraries, through anyio. AnyIO pytest plugin is used in replacement for pytest-asyncio: - either the pytest.mark.asyncio is replaced by pytest.mark.anyio, or, - we rely on the 'anyio_backend' fixture that is pulled in 'aconn_cls' fixture (and hence 'aconn') providing automatic detection for test functions using it. The 'anyio_backend' fixture is parametrized to only use asyncio and selects the event loop policy we need on Windows platform as previously done in pytest_sessionstart(), but only for Python version 3.8 or higher. This fixture is defined in main conftest.py, as well as in pool/conftest.py since we'll change the former to support more async backend while keeping the later asyncio-only for now. Function test_concurrency_async.py::test_ctrl_c is no longer 'async' because its code does not directly use asyncio (it's done through a subprocess); but the 'async def' was needed before in order for pytest-asyncio to run it since the test module had a global pytest.mark.asyncio (and we were using the "auto" mode). --- psycopg/setup.cfg | 2 +- pyproject.toml | 1 - tests/conftest.py | 26 ++++++++++++++++---------- tests/constraints.txt | 2 +- tests/crdb/test_connection_async.py | 2 +- tests/crdb/test_copy_async.py | 2 +- tests/crdb/test_cursor_async.py | 2 +- tests/fix_db.py | 2 +- tests/pool/conftest.py | 14 ++++++++++++++ tests/pool/test_null_pool_async.py | 2 +- tests/pool/test_pool_async.py | 2 +- tests/pool/test_sched_async.py | 2 +- tests/test_client_cursor_async.py | 2 +- tests/test_concurrency_async.py | 4 +--- tests/test_connection_async.py | 2 +- tests/test_conninfo.py | 6 +++--- tests/test_copy_async.py | 1 - tests/test_cursor_async.py | 1 - tests/test_dns.py | 2 +- tests/test_dns_srv.py | 4 ++-- tests/test_errors.py | 1 - tests/test_pipeline_async.py | 1 - tests/test_prepared_async.py | 2 -- tests/test_server_cursor_async.py | 1 - tests/test_tpc_async.py | 1 - tests/test_transaction_async.py | 4 +--- tests/test_typeinfo.py | 2 -- tests/test_waiting.py | 10 +++++----- tests/types/test_composite.py | 1 - tests/types/test_enum.py | 1 - tests/types/test_multirange.py | 2 -- tests/types/test_range.py | 2 -- 32 files changed, 54 insertions(+), 55 deletions(-) create mode 100644 tests/pool/conftest.py diff --git a/psycopg/setup.cfg b/psycopg/setup.cfg index 089fa4ee2..63abf750b 100644 --- a/psycopg/setup.cfg +++ b/psycopg/setup.cfg @@ -65,10 +65,10 @@ binary = pool = psycopg-pool test = + anyio >= 3.6.2 mypy >= 0.990 pproxy >= 2.7 pytest >= 6.2.5 - pytest-asyncio >= 0.17 pytest-cov >= 3.0 pytest-randomly >= 3.5 dev = diff --git a/pyproject.toml b/pyproject.toml index 14f3c9e0d..950bccb4a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,7 +3,6 @@ requires = ["setuptools>=49.2.0", "wheel>=0.37"] build-backend = "setuptools.build_meta" [tool.pytest.ini_options] -asyncio_mode = "auto" filterwarnings = [ "error", ] diff --git a/tests/conftest.py b/tests/conftest.py index 15bcf4099..1ec997bf9 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,7 +1,9 @@ import sys import asyncio import selectors -from typing import List +from typing import Any, Dict, List + +import pytest pytest_plugins = ( "tests.fix_db", @@ -64,17 +66,21 @@ def pytest_sessionstart(session): raise session.Failed cache.set("segfault", True) - # Configure the async loop. - loop = session.config.getoption("--loop") - if loop == "uvloop": - import uvloop - uvloop.install() - else: - assert loop == "default" +asyncio_options: Dict[str, Any] = {} +if sys.platform == "win32" and sys.version_info >= (3, 8): + asyncio_options["policy"] = asyncio.WindowsSelectorEventLoopPolicy() + - if sys.platform == "win32": - asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) +@pytest.fixture( + params=[pytest.param(("asyncio", asyncio_options.copy()), id="asyncio")], + scope="session", +) +def anyio_backend(request): + backend, options = request.param + if request.config.option.loop == "uvloop": + options["use_uvloop"] = True + return backend, options allow_fail_messages: List[str] = [] diff --git a/tests/constraints.txt b/tests/constraints.txt index a254f2907..d4b9ffc06 100644 --- a/tests/constraints.txt +++ b/tests/constraints.txt @@ -9,10 +9,10 @@ typing-extensions == 4.1.0 importlib-metadata == 1.4 # From the 'test' extra +anyio == 3.6.2 mypy == 0.990 pproxy == 2.7.0 pytest == 6.2.5 -pytest-asyncio == 0.17.0 pytest-cov == 3.0.0 pytest-randomly == 3.5.0 diff --git a/tests/crdb/test_connection_async.py b/tests/crdb/test_connection_async.py index b568e426e..9cd73ddd7 100644 --- a/tests/crdb/test_connection_async.py +++ b/tests/crdb/test_connection_async.py @@ -8,7 +8,7 @@ from psycopg._compat import create_task import pytest -pytestmark = [pytest.mark.crdb, pytest.mark.asyncio] +pytestmark = [pytest.mark.crdb, pytest.mark.anyio] async def test_is_crdb(aconn): diff --git a/tests/crdb/test_copy_async.py b/tests/crdb/test_copy_async.py index d5fbf50d0..45ee5eca0 100644 --- a/tests/crdb/test_copy_async.py +++ b/tests/crdb/test_copy_async.py @@ -13,7 +13,7 @@ from ..test_copy import sample_records from ..test_copy_async import ensure_table from .test_copy import sample_tabledef, copyopt -pytestmark = [pytest.mark.crdb, pytest.mark.asyncio] +pytestmark = [pytest.mark.crdb, pytest.mark.anyio] @pytest.mark.parametrize( diff --git a/tests/crdb/test_cursor_async.py b/tests/crdb/test_cursor_async.py index fcc7760a9..2472c37f2 100644 --- a/tests/crdb/test_cursor_async.py +++ b/tests/crdb/test_cursor_async.py @@ -12,7 +12,7 @@ from .test_cursor import testfeed testfeed # fixture -pytestmark = [pytest.mark.crdb, pytest.mark.asyncio] +pytestmark = [pytest.mark.crdb, pytest.mark.anyio] @pytest.mark.slow diff --git a/tests/fix_db.py b/tests/fix_db.py index 3a37aa10a..890e4ed5a 100644 --- a/tests/fix_db.py +++ b/tests/fix_db.py @@ -239,7 +239,7 @@ def conn_cls(session_dsn): @pytest.fixture(scope="session") -def aconn_cls(session_dsn): +def aconn_cls(session_dsn, anyio_backend): cls = psycopg.AsyncConnection if crdb_version: from psycopg.crdb import AsyncCrdbConnection diff --git a/tests/pool/conftest.py b/tests/pool/conftest.py new file mode 100644 index 000000000..a4d1f3564 --- /dev/null +++ b/tests/pool/conftest.py @@ -0,0 +1,14 @@ +import pytest + +from ..conftest import asyncio_options + + +@pytest.fixture( + params=[pytest.param(("asyncio", asyncio_options.copy()), id="asyncio")], + scope="session", +) +def anyio_backend(request): + backend, options = request.param + if request.config.option.loop == "uvloop": + options["use_uvloop"] = True + return backend, options diff --git a/tests/pool/test_null_pool_async.py b/tests/pool/test_null_pool_async.py index d33eecb4d..fea47fbf4 100644 --- a/tests/pool/test_null_pool_async.py +++ b/tests/pool/test_null_pool_async.py @@ -11,7 +11,7 @@ from psycopg.pq import TransactionStatus from psycopg._compat import create_task from .test_pool_async import delay_connection, ensure_waiting -pytestmark = [pytest.mark.asyncio] +pytestmark = [pytest.mark.anyio] try: from psycopg_pool import AsyncNullConnectionPool # noqa: F401 diff --git a/tests/pool/test_pool_async.py b/tests/pool/test_pool_async.py index 27f9f452c..1f16ae2f3 100644 --- a/tests/pool/test_pool_async.py +++ b/tests/pool/test_pool_async.py @@ -15,7 +15,7 @@ except ImportError: # Tests should have been skipped if the package is not available pass -pytestmark = [pytest.mark.asyncio] +pytestmark = [pytest.mark.anyio] async def test_defaults(dsn): diff --git a/tests/pool/test_sched_async.py b/tests/pool/test_sched_async.py index 492d62059..28df510c2 100644 --- a/tests/pool/test_sched_async.py +++ b/tests/pool/test_sched_async.py @@ -13,7 +13,7 @@ except ImportError: # Tests should have been skipped if the package is not available pass -pytestmark = [pytest.mark.asyncio, pytest.mark.timing] +pytestmark = [pytest.mark.anyio, pytest.mark.timing] @pytest.mark.slow diff --git a/tests/test_client_cursor_async.py b/tests/test_client_cursor_async.py index 50f08f7ef..ec6504d07 100644 --- a/tests/test_client_cursor_async.py +++ b/tests/test_client_cursor_async.py @@ -14,7 +14,7 @@ from .test_cursor import execmany, _execmany # noqa: F401 from .fix_crdb import crdb_encoding execmany = execmany # avoid F811 underneath -pytestmark = pytest.mark.asyncio +pytestmark = pytest.mark.anyio @pytest.fixture diff --git a/tests/test_concurrency_async.py b/tests/test_concurrency_async.py index 29b08cf1b..67bb6afbd 100644 --- a/tests/test_concurrency_async.py +++ b/tests/test_concurrency_async.py @@ -12,8 +12,6 @@ import psycopg from psycopg import errors as e from psycopg._compat import create_task -pytestmark = pytest.mark.asyncio - @pytest.mark.slow async def test_commit_concurrency(aconn): @@ -194,7 +192,7 @@ async def test_identify_closure(aconn_cls, dsn): sys.platform == "win32", reason="don't know how to Ctrl-C on Windows" ) @pytest.mark.crdb_skip("cancel") -async def test_ctrl_c(dsn): +def test_ctrl_c(dsn): script = f"""\ import signal import asyncio diff --git a/tests/test_connection_async.py b/tests/test_connection_async.py index 1288a6c81..fe1a403f1 100644 --- a/tests/test_connection_async.py +++ b/tests/test_connection_async.py @@ -17,7 +17,7 @@ from .test_connection import testctx # noqa: F401 # fixture from .test_adapt import make_bin_dumper, make_dumper from .test_conninfo import fake_resolve # noqa: F401 -pytestmark = pytest.mark.asyncio +pytestmark = pytest.mark.anyio async def test_connect(aconn_cls, dsn): diff --git a/tests/test_conninfo.py b/tests/test_conninfo.py index e2c2c01b9..56a944ff1 100644 --- a/tests/test_conninfo.py +++ b/tests/test_conninfo.py @@ -348,7 +348,7 @@ class TestConnectionInfo: ), ], ) -@pytest.mark.asyncio +@pytest.mark.anyio async def test_resolve_hostaddr_async_no_resolve( setpgenv, conninfo, want, env, fail_resolve ): @@ -398,7 +398,7 @@ async def test_resolve_hostaddr_async_no_resolve( ), ], ) -@pytest.mark.asyncio +@pytest.mark.anyio async def test_resolve_hostaddr_async(conninfo, want, env, fake_resolve): params = conninfo_to_dict(conninfo) params = await resolve_hostaddr_async(params) @@ -414,7 +414,7 @@ async def test_resolve_hostaddr_async(conninfo, want, env, fake_resolve): ("host=1.1.1.1,2.2.2.2", {"PGPORT": "1,2,3"}), ], ) -@pytest.mark.asyncio +@pytest.mark.anyio async def test_resolve_hostaddr_async_bad(setpgenv, conninfo, env, fake_resolve): setpgenv(env) params = conninfo_to_dict(conninfo) diff --git a/tests/test_copy_async.py b/tests/test_copy_async.py index 59389dd73..79e97c0c6 100644 --- a/tests/test_copy_async.py +++ b/tests/test_copy_async.py @@ -24,7 +24,6 @@ from .test_copy import sample_values, sample_records, sample_tabledef from .test_copy import py_to_raw, special_chars pytestmark = [ - pytest.mark.asyncio, pytest.mark.crdb_skip("copy"), ] diff --git a/tests/test_cursor_async.py b/tests/test_cursor_async.py index fdc3d898f..e20815355 100644 --- a/tests/test_cursor_async.py +++ b/tests/test_cursor_async.py @@ -13,7 +13,6 @@ from .test_cursor import execmany, _execmany # noqa: F401 from .fix_crdb import crdb_encoding execmany = execmany # avoid F811 underneath -pytestmark = pytest.mark.asyncio async def test_init(aconn): diff --git a/tests/test_dns.py b/tests/test_dns.py index f50092ffb..2eb5569df 100644 --- a/tests/test_dns.py +++ b/tests/test_dns.py @@ -6,7 +6,7 @@ from psycopg.conninfo import conninfo_to_dict pytestmark = [pytest.mark.dns] -@pytest.mark.asyncio +@pytest.mark.anyio async def test_resolve_hostaddr_async_warning(recwarn): import_dnspython() conninfo = "dbname=foo" diff --git a/tests/test_dns_srv.py b/tests/test_dns_srv.py index 857491ccd..524427131 100644 --- a/tests/test_dns_srv.py +++ b/tests/test_dns_srv.py @@ -52,7 +52,7 @@ def test_srv(conninfo, want, env, fake_srv, setpgenv): assert conninfo_to_dict(want) == params -@pytest.mark.asyncio +@pytest.mark.anyio @pytest.mark.parametrize("conninfo, want, env", samples_ok) async def test_srv_async(conninfo, want, env, afake_srv, setpgenv): setpgenv(env) @@ -75,7 +75,7 @@ def test_srv_bad(conninfo, env, fake_srv, setpgenv): psycopg._dns.resolve_srv(params) # type: ignore[attr-defined] -@pytest.mark.asyncio +@pytest.mark.anyio @pytest.mark.parametrize("conninfo, env", samples_bad) async def test_srv_bad_async(conninfo, env, afake_srv, setpgenv): setpgenv(env) diff --git a/tests/test_errors.py b/tests/test_errors.py index 23ad314fd..b2ca66c12 100644 --- a/tests/test_errors.py +++ b/tests/test_errors.py @@ -216,7 +216,6 @@ def test_diag_from_commit(conn): assert exc.value.diag.sqlstate == "23503" -@pytest.mark.asyncio @pytest.mark.crdb_skip("deferrable") async def test_diag_from_commit_async(aconn): cur = aconn.cursor() diff --git a/tests/test_pipeline_async.py b/tests/test_pipeline_async.py index 1dc611085..bdaf5a5eb 100644 --- a/tests/test_pipeline_async.py +++ b/tests/test_pipeline_async.py @@ -13,7 +13,6 @@ from psycopg import errors as e from .test_pipeline import pipeline_aborted pytestmark = [ - pytest.mark.asyncio, pytest.mark.pipeline, pytest.mark.skipif("not psycopg.AsyncPipeline.is_supported()"), ] diff --git a/tests/test_prepared_async.py b/tests/test_prepared_async.py index 84d948f65..afe7d4db5 100644 --- a/tests/test_prepared_async.py +++ b/tests/test_prepared_async.py @@ -9,8 +9,6 @@ import pytest from psycopg.rows import namedtuple_row -pytestmark = pytest.mark.asyncio - @pytest.mark.parametrize("value", [None, 0, 3]) async def test_prepare_threshold_init(aconn_cls, dsn, value): diff --git a/tests/test_server_cursor_async.py b/tests/test_server_cursor_async.py index 21b434503..8325639c1 100644 --- a/tests/test_server_cursor_async.py +++ b/tests/test_server_cursor_async.py @@ -5,7 +5,6 @@ from psycopg import rows, errors as e from psycopg.pq import Format pytestmark = [ - pytest.mark.asyncio, pytest.mark.crdb_skip("server-side cursor"), ] diff --git a/tests/test_tpc_async.py b/tests/test_tpc_async.py index a829c4c12..05a547c5d 100644 --- a/tests/test_tpc_async.py +++ b/tests/test_tpc_async.py @@ -4,7 +4,6 @@ import psycopg from psycopg.pq import TransactionStatus pytestmark = [ - pytest.mark.asyncio, pytest.mark.crdb_skip("2-phase commit"), ] diff --git a/tests/test_transaction_async.py b/tests/test_transaction_async.py index 55e1c9c79..921ea5a14 100644 --- a/tests/test_transaction_async.py +++ b/tests/test_transaction_async.py @@ -11,11 +11,9 @@ from .test_transaction import in_transaction, insert_row, inserted, get_exc_info from .test_transaction import ExpectedException, crdb_skip_external_observer from .test_transaction import create_test_table # noqa # autouse fixture -pytestmark = pytest.mark.asyncio - @pytest.fixture -async def aconn(aconn, apipeline): +async def aconn(aconn, apipeline, anyio_backend): return aconn diff --git a/tests/test_typeinfo.py b/tests/test_typeinfo.py index 1d74ef2bf..aaed8d048 100644 --- a/tests/test_typeinfo.py +++ b/tests/test_typeinfo.py @@ -42,7 +42,6 @@ def test_fetch(conn, name, status, encoding): assert info.regtype == "text" -@pytest.mark.asyncio @pytest.mark.parametrize("name", ["text", sql.Identifier("text")]) @pytest.mark.parametrize("status", ["IDLE", "INTRANS", None]) @pytest.mark.parametrize( @@ -111,7 +110,6 @@ def test_fetch_not_found(conn, name, status, info_cls, monkeypatch): assert info is None -@pytest.mark.asyncio @_name @_status @_info_cls diff --git a/tests/test_waiting.py b/tests/test_waiting.py index 63237e879..f5ece29e5 100644 --- a/tests/test_waiting.py +++ b/tests/test_waiting.py @@ -114,21 +114,21 @@ def test_wait_large_fd(dsn, waitfn): @pytest.mark.parametrize("timeout", timeouts) -@pytest.mark.asyncio +@pytest.mark.anyio async def test_wait_conn_async(dsn, timeout): gen = generators.connect(dsn) conn = await waiting.wait_conn_async(gen, **timeout) assert conn.status == ConnStatus.OK -@pytest.mark.asyncio +@pytest.mark.anyio async def test_wait_conn_async_bad(dsn): gen = generators.connect("dbname=nosuchdb") with pytest.raises(psycopg.OperationalError): await waiting.wait_conn_async(gen) -@pytest.mark.asyncio +@pytest.mark.anyio @pytest.mark.parametrize("wait, ready", zip(waiting.Wait, waiting.Ready)) @skip_if_not_linux async def test_wait_ready_async(wait, ready): @@ -141,7 +141,7 @@ async def test_wait_ready_async(wait, ready): assert r & ready -@pytest.mark.asyncio +@pytest.mark.anyio async def test_wait_async(pgconn): pgconn.send_query(b"select 1") gen = generators.execute(pgconn) @@ -149,7 +149,7 @@ async def test_wait_async(pgconn): assert res.status == ExecStatus.TUPLES_OK -@pytest.mark.asyncio +@pytest.mark.anyio async def test_wait_async_bad(pgconn): pgconn.send_query(b"select 1") gen = generators.execute(pgconn) diff --git a/tests/types/test_composite.py b/tests/types/test_composite.py index 47beecf35..49e734bf4 100644 --- a/tests/types/test_composite.py +++ b/tests/types/test_composite.py @@ -166,7 +166,6 @@ def test_fetch_info(conn, testcomp, name, fields): assert info.field_types[i] == builtins[t].oid -@pytest.mark.asyncio @pytest.mark.parametrize("name, fields", fetch_cases) async def test_fetch_info_async(aconn, testcomp, name, fields): info = await CompositeInfo.fetch(aconn, name) diff --git a/tests/types/test_enum.py b/tests/types/test_enum.py index 8dfb6d4f0..e40c61025 100644 --- a/tests/types/test_enum.py +++ b/tests/types/test_enum.py @@ -68,7 +68,6 @@ def test_fetch_info(conn): assert info.labels == list(StrTestEnum.__members__) -@pytest.mark.asyncio async def test_fetch_info_async(aconn): info = await EnumInfo.fetch(aconn, "PureTestEnum") assert info.name == "puretestenum" diff --git a/tests/types/test_multirange.py b/tests/types/test_multirange.py index 2ab51524e..65ff0fe0e 100644 --- a/tests/types/test_multirange.py +++ b/tests/types/test_multirange.py @@ -387,7 +387,6 @@ def test_fetch_info_not_found(conn): assert MultirangeInfo.fetch(conn, "nosuchrange") is None -@pytest.mark.asyncio @pytest.mark.parametrize("name, subtype", fetch_cases) async def test_fetch_info_async(aconn, testmr, name, subtype): # noqa: F811 info = await MultirangeInfo.fetch(aconn, name) @@ -397,7 +396,6 @@ async def test_fetch_info_async(aconn, testmr, name, subtype): # noqa: F811 assert info.subtype_oid == aconn.adapters.types[subtype].oid -@pytest.mark.asyncio async def test_fetch_info_not_found_async(aconn): assert await MultirangeInfo.fetch(aconn, "nosuchrange") is None diff --git a/tests/types/test_range.py b/tests/types/test_range.py index 1efd398bb..b1026d302 100644 --- a/tests/types/test_range.py +++ b/tests/types/test_range.py @@ -303,7 +303,6 @@ def test_fetch_info_not_found(conn): assert RangeInfo.fetch(conn, "nosuchrange") is None -@pytest.mark.asyncio @pytest.mark.parametrize("name, subtype", fetch_cases) async def test_fetch_info_async(aconn, testrange, name, subtype): info = await RangeInfo.fetch(aconn, name) @@ -313,7 +312,6 @@ async def test_fetch_info_async(aconn, testrange, name, subtype): assert info.subtype_oid == aconn.adapters.types[subtype].oid -@pytest.mark.asyncio async def test_fetch_info_not_found_async(aconn): assert await RangeInfo.fetch(aconn, "nosuchrange") is None -- 2.47.2