From: Erik Wienhold Date: Wed, 14 Jan 2026 13:42:18 +0000 (+0100) Subject: fix: retain pgconn on OperationalError X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=b5573ec522ef54c60b9986dcede4023deef9c1d1;p=thirdparty%2Fpsycopg.git fix: retain pgconn on OperationalError When multiple connection attempts fail, a new OperationalError with the combined attempt errors is raised without retaining the pgconn attribute of the original attempt errors. Fix this by assigning the pgconn from the last attempt error. --- diff --git a/docs/news.rst b/docs/news.rst index 4ac9cc863..156f9edea 100644 --- a/docs/news.rst +++ b/docs/news.rst @@ -10,6 +10,13 @@ Current release --------------- +Psyocpg 3.3.3 (unreleased) +^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Retain `Error.pgconn` when raising a single exception for multiple connection +attempt errors (:ticket:`#1246`). + + Psycopg 3.3.2 ^^^^^^^^^^^^^ diff --git a/psycopg/psycopg/connection.py b/psycopg/psycopg/connection.py index 0d74066f3..c50a386f9 100644 --- a/psycopg/psycopg/connection.py +++ b/psycopg/psycopg/connection.py @@ -126,7 +126,8 @@ class Connection(BaseConnection[Row]): lines = [str(last_ex)] lines.append("Multiple connection attempts failed. All failures were:") lines.extend((f"- {descr}: {error}" for error, descr in conn_errors)) - raise type(last_ex)("\n".join(lines)).with_traceback(None) + new_ex = type(last_ex)("\n".join(lines), pgconn=last_ex.pgconn) + raise new_ex.with_traceback(None) if ( capabilities.has_used_gssapi() diff --git a/psycopg/psycopg/connection_async.py b/psycopg/psycopg/connection_async.py index 0a10ae24a..3c8edc383 100644 --- a/psycopg/psycopg/connection_async.py +++ b/psycopg/psycopg/connection_async.py @@ -142,7 +142,8 @@ class AsyncConnection(BaseConnection[Row]): lines = [str(last_ex)] lines.append("Multiple connection attempts failed. All failures were:") lines.extend(f"- {descr}: {error}" for error, descr in conn_errors) - raise type(last_ex)("\n".join(lines)).with_traceback(None) + new_ex = type(last_ex)("\n".join(lines), pgconn=last_ex.pgconn) + raise new_ex.with_traceback(None) if ( capabilities.has_used_gssapi() diff --git a/tests/test_connection.py b/tests/test_connection.py index c30fb9bee..8510afcc9 100644 --- a/tests/test_connection.py +++ b/tests/test_connection.py @@ -3,6 +3,7 @@ # DO NOT CHANGE! Change the original file instead. from __future__ import annotations +import os import sys import time import logging @@ -15,7 +16,7 @@ import psycopg from psycopg import errors as e from psycopg import pq from psycopg.rows import tuple_row -from psycopg.conninfo import conninfo_to_dict, timeout_from_conninfo +from psycopg.conninfo import conninfo_to_dict, make_conninfo, timeout_from_conninfo from psycopg._conninfo_utils import get_param from .acompat import skip_async, skip_sync, sleep @@ -80,6 +81,25 @@ def test_connect_error_multi_hosts_each_message_preserved(conn_cls): ) +def test_multi_attempt_pgconn(conn_cls, dsn, monkeypatch): + with conn_cls.connect(dsn) as conn: + if not conn.pgconn.used_password: + pytest.skip("test connection needs no password") + + monkeypatch.delenv("PGPASSWORD", raising=False) + info = conninfo_to_dict(dsn) + info.pop("password", None) + info["host"] = ",".join([str(info.get("host", os.environ.get("PGHOST", "")))] * 2) + dsn = make_conninfo("", **info) + + with pytest.raises(psycopg.OperationalError, match="connection failed:") as e: + conn_cls.connect(dsn) + + msg = str(e.value) + assert MULTI_FAILURE_MESSAGE in msg + assert e.value.pgconn + + def test_connect_str_subclass(conn_cls, dsn): class MyString(str): diff --git a/tests/test_connection_async.py b/tests/test_connection_async.py index 99a7ff41e..122228a49 100644 --- a/tests/test_connection_async.py +++ b/tests/test_connection_async.py @@ -1,5 +1,6 @@ from __future__ import annotations +import os import sys import time import logging @@ -12,7 +13,7 @@ import psycopg from psycopg import errors as e from psycopg import pq from psycopg.rows import tuple_row -from psycopg.conninfo import conninfo_to_dict, timeout_from_conninfo +from psycopg.conninfo import conninfo_to_dict, make_conninfo, timeout_from_conninfo from psycopg._conninfo_utils import get_param from .acompat import asleep, skip_async, skip_sync @@ -76,6 +77,25 @@ async def test_connect_error_multi_hosts_each_message_preserved(aconn_cls): assert any(expected_host2 in line and expected_error in line for line in msg_lines) +async def test_multi_attempt_pgconn(aconn_cls, dsn, monkeypatch): + async with await aconn_cls.connect(dsn) as conn: + if not conn.pgconn.used_password: + pytest.skip("test connection needs no password") + + monkeypatch.delenv("PGPASSWORD", raising=False) + info = conninfo_to_dict(dsn) + info.pop("password", None) + info["host"] = ",".join([str(info.get("host", os.environ.get("PGHOST", "")))] * 2) + dsn = make_conninfo("", **info) + + with pytest.raises(psycopg.OperationalError, match="connection failed:") as e: + await aconn_cls.connect(dsn) + + msg = str(e.value) + assert MULTI_FAILURE_MESSAGE in msg + assert e.value.pgconn + + async def test_connect_str_subclass(aconn_cls, dsn): class MyString(str): pass