From: Daniele Varrazzo Date: Mon, 7 Aug 2023 23:16:19 +0000 (+0100) Subject: refactor(tests): auto-generate test_connection from async X-Git-Tag: pool-3.2.0~12^2~59 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=47143d9bdf69b81a714517d41247dc69522148d2;p=thirdparty%2Fpsycopg.git refactor(tests): auto-generate test_connection from async --- diff --git a/tests/_test_connection.py b/tests/_test_connection.py index 296a7f7f4..8ce090293 100644 --- a/tests/_test_connection.py +++ b/tests/_test_connection.py @@ -9,6 +9,22 @@ import pytest import psycopg +async def aconn_set(conn, param, value): + """Equivalent of 'await conn.set_param(value)' + + Converted to conn_set in sync tests. + """ + await getattr(conn, f"set_{param}")(value) + + +def conn_set(conn, param, value): + """Equivalent of 'conn.param = value'. + + Converted from aconn_set in sync tests. + """ + setattr(conn, param, value) + + @pytest.fixture def testctx(svcconn): svcconn.execute("create table if not exists testctx (id int primary key)") diff --git a/tests/test_connection.py b/tests/test_connection.py index a15a21ec9..9516281b8 100644 --- a/tests/test_connection.py +++ b/tests/test_connection.py @@ -1,3 +1,6 @@ +# WARNING: this file is auto-generated by 'async_to_sync.py' +# from the original file 'test_connection_async.py' +# DO NOT CHANGE! Change the original file instead. import sys import time import pytest @@ -10,10 +13,10 @@ from psycopg import Notify, pq, errors as e from psycopg.rows import tuple_row from psycopg.conninfo import conninfo_to_dict, make_conninfo -from .utils import gc_collect +from .utils import gc_collect, is_async from ._test_cursor import my_row_factory from ._test_connection import tx_params, tx_params_isolation, tx_values_map -from ._test_connection import conninfo_params_timeout +from ._test_connection import conninfo_params_timeout, conn_set from ._test_connection import testctx # noqa: F401 # fixture from .test_adapt import make_bin_dumper, make_dumper @@ -93,8 +96,12 @@ def test_cursor_closed(conn): # TODO: the INERROR started failing in the C implementation in Python 3.12a7 # compiled with Cython-3.0.0b3, not before. + + @pytest.mark.xfail( - (pq.__impl__ in ("c", "binary") and sys.version_info[:2] == (3, 12)), + pq.__impl__ in ("c", "binary") + and sys.version_info[:2] == (3, 12) + and (not is_async(__name__)), reason="Something with Exceptions, C, Python 3.12", ) def test_connection_warn_close(conn_cls, dsn, recwarn): @@ -173,8 +180,7 @@ def test_context_inerror_rollback_no_clobber(conn_cls, conn, dsn, caplog): with conn_cls.connect(dsn) as conn2: conn2.execute("select 1") conn.execute( - "select pg_terminate_backend(%s::int)", - [conn2.pgconn.backend_pid], + "select pg_terminate_backend(%s::int)", [conn2.pgconn.backend_pid] ) 1 / 0 @@ -230,14 +236,14 @@ def test_commit(conn): @pytest.mark.crdb_skip("deferrable") def test_commit_error(conn): - conn.execute( - """ - drop table if exists selfref; - create table selfref ( - x serial primary key, - y int references selfref (x) deferrable initially deferred) - """ - ) + sql = [ + "drop table if exists selfref;", + "create table selfref (", + "x serial primary key,", + "y int references selfref (x) deferrable initially deferred)", + ] + + conn.execute("".join(sql)) conn.commit() conn.execute("insert into selfref (y) values (-1)") @@ -276,7 +282,8 @@ def test_auto_transaction(conn): conn.commit() assert conn.pgconn.transaction_status == conn.TransactionStatus.IDLE - assert cur.execute("select * from foo").fetchone() == (1,) + cur.execute("select * from foo") + assert cur.fetchone() == (1,) assert conn.pgconn.transaction_status == conn.TransactionStatus.INTRANS @@ -299,21 +306,34 @@ def test_auto_transaction_fail(conn): conn.commit() assert conn.pgconn.transaction_status == conn.TransactionStatus.IDLE - assert cur.execute("select * from foo").fetchone() is None + cur.execute("select * from foo") + assert cur.fetchone() is None assert conn.pgconn.transaction_status == conn.TransactionStatus.INTRANS +@pytest.mark.skipif(not is_async(__name__), reason="async test only") +def test_autocommit_readonly_property(conn): + with pytest.raises(AttributeError): + conn.autocommit = True + assert not conn.autocommit + + def test_autocommit(conn): assert conn.autocommit is False + conn.autocommit = True assert conn.autocommit cur = conn.cursor() - assert cur.execute("select 1").fetchone() == (1,) + cur.execute("select 1") + assert cur.fetchone() == (1,) assert conn.pgconn.transaction_status == conn.TransactionStatus.IDLE conn.autocommit = "" - assert conn.autocommit is False # type: ignore[comparison-overlap] + assert isinstance(conn.autocommit, bool) + assert conn.autocommit is False + conn.autocommit = "yeah" + assert isinstance(conn.autocommit, bool) assert conn.autocommit is True @@ -325,7 +345,8 @@ def test_autocommit_connect(conn_cls, dsn): def test_autocommit_intrans(conn): cur = conn.cursor() - assert cur.execute("select 1").fetchone() == (1,) + cur.execute("select 1") + assert cur.fetchone() == (1,) assert conn.pgconn.transaction_status == conn.TransactionStatus.INTRANS with pytest.raises(psycopg.ProgrammingError): conn.autocommit = True @@ -355,17 +376,17 @@ def test_autocommit_unknown(conn): [ ((), {}, ""), (("",), {}, ""), - (("host=foo user=bar",), {}, "host=foo user=bar"), - (("host=foo",), {"user": "baz"}, "host=foo user=baz"), + (("dbname=foo user=bar",), {}, "dbname=foo user=bar"), + (("dbname=foo",), {"user": "baz"}, "dbname=foo user=baz"), ( - ("host=foo port=5432",), - {"host": "qux", "user": "joe"}, - "host=qux user=joe port=5432", + ("dbname=foo port=5432",), + {"dbname": "qux", "user": "joe"}, + "dbname=qux user=joe port=5432", ), - (("host=foo",), {"user": None}, "host=foo"), + (("dbname=foo",), {"user": None}, "dbname=foo"), ], ) -def test_connect_args(conn_cls, monkeypatch, pgconn, args, kwargs, want): +def test_connect_args(conn_cls, monkeypatch, setpgenv, pgconn, args, kwargs, want): the_conninfo: str def fake_connect(conninfo): @@ -374,6 +395,7 @@ def test_connect_args(conn_cls, monkeypatch, pgconn, args, kwargs, want): return pgconn yield + setpgenv({}) monkeypatch.setattr(psycopg.connection, "connect", fake_connect) conn = conn_cls.connect(*args, **kwargs) assert conninfo_to_dict(the_conninfo) == conninfo_to_dict(want) @@ -587,23 +609,32 @@ def test_server_cursor_factory(conn): @pytest.mark.parametrize("param", tx_params) def test_transaction_param_default(conn, param): assert getattr(conn, param.name) is None - current, default = conn.execute( + cur = conn.execute( "select current_setting(%s), current_setting(%s)", [f"transaction_{param.guc}", f"default_transaction_{param.guc}"], - ).fetchone() + ) + (current, default) = cur.fetchone() assert current == default +@pytest.mark.skipif(not is_async(__name__), reason="async test only") +@pytest.mark.parametrize("param", tx_params) +def test_transaction_param_readonly_property(conn, param): + with pytest.raises(AttributeError): + setattr(conn, param.name, None) + + @pytest.mark.parametrize("autocommit", [True, False]) @pytest.mark.parametrize("param", tx_params_isolation) def test_set_transaction_param_implicit(conn, param, autocommit): conn.autocommit = autocommit for value in param.values: - setattr(conn, param.name, value) - pgval, default = conn.execute( + conn_set(conn, param.name, value) + cur = conn.execute( "select current_setting(%s), current_setting(%s)", [f"transaction_{param.guc}", f"default_transaction_{param.guc}"], - ).fetchone() + ) + (pgval, default) = cur.fetchone() if autocommit: assert pgval == default else: @@ -620,17 +651,15 @@ def test_set_transaction_param_reset(conn, param): conn.commit() for value in param.values: - setattr(conn, param.name, value) - (pgval,) = conn.execute( - "select current_setting(%s)", [f"transaction_{param.guc}"] - ).fetchone() + conn_set(conn, param.name, value) + cur = conn.execute("select current_setting(%s)", [f"transaction_{param.guc}"]) + (pgval,) = cur.fetchone() assert tx_values_map[pgval] == value conn.rollback() - setattr(conn, param.name, None) - (pgval,) = conn.execute( - "select current_setting(%s)", [f"transaction_{param.guc}"] - ).fetchone() + conn_set(conn, param.name, None) + cur = conn.execute("select current_setting(%s)", [f"transaction_{param.guc}"]) + (pgval,) = cur.fetchone() assert tx_values_map[pgval] == tx_values_map[param.non_default] conn.rollback() @@ -640,34 +669,39 @@ def test_set_transaction_param_reset(conn, param): def test_set_transaction_param_block(conn, param, autocommit): conn.autocommit = autocommit for value in param.values: - setattr(conn, param.name, value) + conn_set(conn, param.name, value) with conn.transaction(): - pgval = conn.execute( + cur = conn.execute( "select current_setting(%s)", [f"transaction_{param.guc}"] - ).fetchone()[0] + ) + pgval = cur.fetchone()[0] assert tx_values_map[pgval] == value @pytest.mark.parametrize("param", tx_params) def test_set_transaction_param_not_intrans_implicit(conn, param): conn.execute("select 1") + value = param.values[0] with pytest.raises(psycopg.ProgrammingError): - setattr(conn, param.name, param.values[0]) + conn_set(conn, param.name, value) @pytest.mark.parametrize("param", tx_params) def test_set_transaction_param_not_intrans_block(conn, param): + value = param.values[0] with conn.transaction(): with pytest.raises(psycopg.ProgrammingError): - setattr(conn, param.name, param.values[0]) + conn_set(conn, param.name, value) @pytest.mark.parametrize("param", tx_params) def test_set_transaction_param_not_intrans_external(conn, param): + value = param.values[0] + conn.autocommit = True conn.execute("begin") with pytest.raises(psycopg.ProgrammingError): - setattr(conn, param.name, param.values[0]) + conn_set(conn, param.name, value) @pytest.mark.crdb("skip", reason="transaction isolation") @@ -677,12 +711,11 @@ def test_set_transaction_param_all(conn): for param in params: value = param.values[0] - setattr(conn, param.name, value) + conn_set(conn, param.name, value) for param in params: - pgval = conn.execute( - "select current_setting(%s)", [f"transaction_{param.guc}"] - ).fetchone()[0] + cur = conn.execute("select current_setting(%s)", [f"transaction_{param.guc}"]) + pgval = cur.fetchone()[0] assert tx_values_map[pgval] == value @@ -702,14 +735,15 @@ def test_set_transaction_param_strange(conn): @pytest.mark.parametrize("dsn, kwargs, exp", conninfo_params_timeout) -def test_get_connection_params(conn_cls, dsn, kwargs, exp): +def test_get_connection_params(conn_cls, dsn, kwargs, exp, setpgenv): + setpgenv({}) params = conn_cls._get_connection_params(dsn, **kwargs) conninfo = make_conninfo(**params) assert conninfo_to_dict(conninfo) == exp[0] - assert params.get("connect_timeout") == exp[1] + assert params["connect_timeout"] == exp[1] -def test_connect_context(conn_cls, dsn): +def test_connect_context_adapters(conn_cls, dsn): ctx = psycopg.adapt.AdaptersMap(psycopg.adapters) ctx.register_dumper(str, make_bin_dumper("b")) ctx.register_dumper(str, make_dumper("t")) diff --git a/tests/test_connection_async.py b/tests/test_connection_async.py index 4b1d797eb..98edb2d21 100644 --- a/tests/test_connection_async.py +++ b/tests/test_connection_async.py @@ -1,21 +1,21 @@ +import sys import time import pytest import logging import weakref -from typing import List, Any +from typing import Any, List import psycopg -from psycopg import Notify, errors as e +from psycopg import Notify, pq, errors as e from psycopg.rows import tuple_row from psycopg.conninfo import conninfo_to_dict, make_conninfo -from .utils import gc_collect +from .utils import gc_collect, is_async from ._test_cursor import my_row_factory from ._test_connection import tx_params, tx_params_isolation, tx_values_map -from ._test_connection import conninfo_params_timeout +from ._test_connection import conninfo_params_timeout, aconn_set 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 # fixture async def test_connect(aconn_cls, dsn): @@ -93,6 +93,14 @@ async def test_cursor_closed(aconn): aconn.cursor() +# TODO: the INERROR started failing in the C implementation in Python 3.12a7 +# compiled with Cython-3.0.0b3, not before. +@pytest.mark.xfail( + pq.__impl__ in ("c", "binary") + and sys.version_info[:2] == (3, 12) + and not is_async(__name__), + reason="Something with Exceptions, C, Python 3.12", +) async def test_connection_warn_close(aconn_cls, dsn, recwarn): conn = await aconn_cls.connect(dsn) await conn.close() @@ -226,14 +234,14 @@ async def test_commit(aconn): @pytest.mark.crdb_skip("deferrable") async def test_commit_error(aconn): - await aconn.execute( - """ - drop table if exists selfref; - create table selfref ( - x serial primary key, - y int references selfref (x) deferrable initially deferred) - """ - ) + sql = [ + "drop table if exists selfref;", + "create table selfref (", + "x serial primary key,", + "y int references selfref (x) deferrable initially deferred)", + ] + + await aconn.execute("".join(sql)) await aconn.commit() await aconn.execute("insert into selfref (y) values (-1)") @@ -301,12 +309,15 @@ async def test_auto_transaction_fail(aconn): assert aconn.pgconn.transaction_status == aconn.TransactionStatus.INTRANS -async def test_autocommit(aconn): - assert aconn.autocommit is False +@pytest.mark.skipif(not is_async(__name__), reason="async test only") +async def test_autocommit_readonly_property(aconn): with pytest.raises(AttributeError): aconn.autocommit = True assert not aconn.autocommit + +async def test_autocommit(aconn): + assert aconn.autocommit is False await aconn.set_autocommit(True) assert aconn.autocommit cur = aconn.cursor() @@ -315,8 +326,11 @@ async def test_autocommit(aconn): assert aconn.pgconn.transaction_status == aconn.TransactionStatus.IDLE await aconn.set_autocommit("") + assert isinstance(aconn.autocommit, bool) assert aconn.autocommit is False + await aconn.set_autocommit("yeah") + assert isinstance(aconn.autocommit, bool) assert aconn.autocommit is True @@ -492,6 +506,7 @@ async def test_notify_handlers(aconn): assert n.channel == "foo" assert n.payload == "n2" assert n.pid == aconn.pgconn.backend_pid + assert hash(n) with pytest.raises(ValueError): aconn.remove_notify_handler(cb1) @@ -603,6 +618,7 @@ async def test_transaction_param_default(aconn, param): assert current == default +@pytest.mark.skipif(not is_async(__name__), reason="async test only") @pytest.mark.parametrize("param", tx_params) async def test_transaction_param_readonly_property(aconn, param): with pytest.raises(AttributeError): @@ -614,7 +630,7 @@ async def test_transaction_param_readonly_property(aconn, param): async def test_set_transaction_param_implicit(aconn, param, autocommit): await aconn.set_autocommit(autocommit) for value in param.values: - await getattr(aconn, f"set_{param.name}")(value) + await aconn_set(aconn, param.name, value) cur = await aconn.execute( "select current_setting(%s), current_setting(%s)", [f"transaction_{param.guc}", f"default_transaction_{param.guc}"], @@ -636,7 +652,7 @@ async def test_set_transaction_param_reset(aconn, param): await aconn.commit() for value in param.values: - await getattr(aconn, f"set_{param.name}")(value) + await aconn_set(aconn, param.name, value) cur = await aconn.execute( "select current_setting(%s)", [f"transaction_{param.guc}"] ) @@ -644,7 +660,7 @@ async def test_set_transaction_param_reset(aconn, param): assert tx_values_map[pgval] == value await aconn.rollback() - await getattr(aconn, f"set_{param.name}")(None) + await aconn_set(aconn, param.name, None) cur = await aconn.execute( "select current_setting(%s)", [f"transaction_{param.guc}"] ) @@ -658,7 +674,7 @@ async def test_set_transaction_param_reset(aconn, param): async def test_set_transaction_param_block(aconn, param, autocommit): await aconn.set_autocommit(autocommit) for value in param.values: - await getattr(aconn, f"set_{param.name}")(value) + await aconn_set(aconn, param.name, value) async with aconn.transaction(): cur = await aconn.execute( "select current_setting(%s)", [f"transaction_{param.guc}"] @@ -672,7 +688,7 @@ async def test_set_transaction_param_not_intrans_implicit(aconn, param): await aconn.execute("select 1") value = param.values[0] with pytest.raises(psycopg.ProgrammingError): - await getattr(aconn, f"set_{param.name}")(value) + await aconn_set(aconn, param.name, value) @pytest.mark.parametrize("param", tx_params) @@ -680,7 +696,7 @@ async def test_set_transaction_param_not_intrans_block(aconn, param): value = param.values[0] async with aconn.transaction(): with pytest.raises(psycopg.ProgrammingError): - await getattr(aconn, f"set_{param.name}")(value) + await aconn_set(aconn, param.name, value) @pytest.mark.parametrize("param", tx_params) @@ -689,7 +705,7 @@ async def test_set_transaction_param_not_intrans_external(aconn, param): await aconn.set_autocommit(True) await aconn.execute("begin") with pytest.raises(psycopg.ProgrammingError): - await getattr(aconn, f"set_{param.name}")(value) + await aconn_set(aconn, param.name, value) @pytest.mark.crdb("skip", reason="transaction isolation") @@ -699,7 +715,7 @@ async def test_set_transaction_param_all(aconn): for param in params: value = param.values[0] - await getattr(aconn, f"set_{param.name}")(value) + await aconn_set(aconn, param.name, value) for param in params: cur = await aconn.execute( @@ -751,33 +767,15 @@ async def test_connect_context_copy(aconn_cls, dsn, aconn): aconn.adapters.register_dumper(str, make_bin_dumper("b")) aconn.adapters.register_dumper(str, make_dumper("t")) - aconn2 = await aconn_cls.connect(dsn, context=aconn) + conn2 = await aconn_cls.connect(dsn, context=aconn) - cur = await aconn2.execute("select %s", ["hello"]) + cur = await conn2.execute("select %s", ["hello"]) assert (await cur.fetchone())[0] == "hellot" - cur = await aconn2.execute("select %b", ["hello"]) + cur = await conn2.execute("select %b", ["hello"]) assert (await cur.fetchone())[0] == "hellob" - await aconn2.close() + await conn2.close() async def test_cancel_closed(aconn): await aconn.close() aconn.cancel() - - -@pytest.mark.usefixtures("fake_resolve") -async def test_resolve_hostaddr_conn(aconn_cls, monkeypatch): - got = [] - - def fake_connect_gen(conninfo, **kwargs): - got.append(conninfo) - 1 / 0 - - monkeypatch.setattr(aconn_cls, "_connect_gen", fake_connect_gen) - - with pytest.raises(ZeroDivisionError): - await aconn_cls.connect("host=foo.com") - - assert len(got) == 1 - want = {"host": "foo.com", "hostaddr": "1.1.1.1"} - assert conninfo_to_dict(got[0]) == want diff --git a/tests/test_dns.py b/tests/test_dns.py index 2eb5569df..b1e889115 100644 --- a/tests/test_dns.py +++ b/tests/test_dns.py @@ -3,9 +3,28 @@ import pytest import psycopg from psycopg.conninfo import conninfo_to_dict -pytestmark = [pytest.mark.dns] +from .test_conninfo import fake_resolve # noqa: F401 # fixture +@pytest.mark.usefixtures("fake_resolve") +async def test_resolve_hostaddr_conn(aconn_cls, monkeypatch): + got = [] + + def fake_connect_gen(conninfo, **kwargs): + got.append(conninfo) + 1 / 0 + + monkeypatch.setattr(aconn_cls, "_connect_gen", fake_connect_gen) + + with pytest.raises(ZeroDivisionError): + await aconn_cls.connect("host=foo.com") + + assert len(got) == 1 + want = {"host": "foo.com", "hostaddr": "1.1.1.1"} + assert conninfo_to_dict(got[0]) == want + + +@pytest.mark.dns @pytest.mark.anyio async def test_resolve_hostaddr_async_warning(recwarn): import_dnspython() diff --git a/tests/utils.py b/tests/utils.py index f20ca7920..64d72fe7f 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -145,6 +145,17 @@ def gc_collect(): gc.collect() +def is_async(obj): + """Return true if obj is an async object (class, instance, module name)""" + if isinstance(obj, str): + # coming from is_async(__name__) + return "async" in obj + + if not isinstance(obj, type): + obj = type(obj) + return "Async" in obj.__name__ + + NO_COUNT_TYPES: Tuple[type, ...] = () if sys.version_info[:2] == (3, 10): diff --git a/tools/async_to_sync.py b/tools/async_to_sync.py index 90ab125aa..e34c7ae43 100755 --- a/tools/async_to_sync.py +++ b/tools/async_to_sync.py @@ -33,7 +33,7 @@ def async_to_sync(tree: ast.AST) -> ast.AST: tree = BlanksInserter().visit(tree) tree = AsyncToSync().visit(tree) tree = RenameAsyncToSync().visit(tree) - tree = FixSetAutocommit().visit(tree) + tree = FixAsyncSetters().visit(tree) return tree @@ -85,9 +85,12 @@ class RenameAsyncToSync(ast.NodeTransformer): "AsyncClientCursor": "ClientCursor", "AsyncCursor": "Cursor", "AsyncRawCursor": "RawCursor", + "AsyncServerCursor": "ServerCursor", "aclose": "close", "aclosing": "closing", "aconn": "conn", + "aconn_cls": "conn_cls", + "aconn_set": "conn_set", "alist": "list", "anext": "next", } @@ -136,24 +139,31 @@ class RenameAsyncToSync(ast.NodeTransformer): return node -class FixSetAutocommit(ast.NodeTransformer): +class FixAsyncSetters(ast.NodeTransformer): + setters_map = { + "set_autocommit": "autocommit", + "set_read_only": "read_only", + "set_isolation_level": "isolation_level", + "set_deferrable": "deferrable", + } + def visit_Call(self, node: ast.Call) -> ast.AST: - new_node = self._fix_autocommit(node) + new_node = self._fix_setter(node) if new_node: return new_node self.generic_visit(node) return node - def _fix_autocommit(self, node: ast.Call) -> ast.AST | None: + def _fix_setter(self, node: ast.Call) -> ast.AST | None: if not isinstance(node.func, ast.Attribute): return None - if node.func.attr != "set_autocommit": + if node.func.attr not in self.setters_map: return None obj = node.func.value arg = node.args[0] new_node = ast.Assign( - targets=[ast.Attribute(value=obj, attr="autocommit")], + targets=[ast.Attribute(value=obj, attr=self.setters_map[node.func.attr])], value=arg, ) ast.copy_location(new_node, node) diff --git a/tools/convert_async_to_sync.sh b/tools/convert_async_to_sync.sh index 93bbc6548..e9b9031e8 100755 --- a/tools/convert_async_to_sync.sh +++ b/tools/convert_async_to_sync.sh @@ -7,5 +7,7 @@ set -euo pipefail dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" cd "${dir}/.." +python "${dir}/async_to_sync.py" tests/test_connection_async.py > tests/test_connection.py +black -q tests/test_connection.py python "${dir}/async_to_sync.py" tests/test_cursor_async.py > tests/test_cursor.py black -q tests/test_cursor.py