From: Daniele Varrazzo Date: Sat, 5 Jul 2025 11:50:02 +0000 (+0200) Subject: feat: add PGconn.full_protocol_version, PGconn.service attributes X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=49528e81808422f34a8a46796edcf3fbe974ec4a;p=thirdparty%2Fpsycopg.git feat: add PGconn.full_protocol_version, PGconn.service attributes Wrap libpq 18 PQfullProtocolVersion and PQservice functions. See #1079 --- diff --git a/docs/conf.py b/docs/conf.py index cf87980d3..0d4e77dee 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -101,7 +101,7 @@ intersphinx_mapping = { autodoc_member_order = "bysource" # PostgreSQL docs version to link libpq functions to -libpq_docs_version = "17" +libpq_docs_version = "18" # Where to point on :ticket: role ticket_url = "https://github.com/psycopg/psycopg/issues/%s" diff --git a/docs/news.rst b/docs/news.rst index a423a1dfc..3599cf298 100644 --- a/docs/news.rst +++ b/docs/news.rst @@ -15,6 +15,8 @@ Psycopg 3.3.0 (unreleased) - Cursors are now iterators, not only iterables. This means you can call ``next(cur)`` to fetch the next row (:ticket:`#1064`). +- Add support for functions :pq:`PQfullProtocolVersion`, :pq:`PQservice`, + available in libpq v18 (:ticket:`#1079`). - Drop support for Python 3.8 (:ticket:`#976`) and 3.9 (:ticket:`#1056`). diff --git a/psycopg/psycopg/errors.py b/psycopg/psycopg/errors.py index f5e0396d1..63deca1b9 100644 --- a/psycopg/psycopg/errors.py +++ b/psycopg/psycopg/errors.py @@ -47,6 +47,7 @@ class FinishedPGconn: info: list[ConninfoOption] = field(default_factory=list) db: bytes = b"" + service: bytes = b"" user: bytes = b"" password: bytes = b"" host: bytes = b"" @@ -61,6 +62,7 @@ class FinishedPGconn: error_message: bytes = b"" _encoding: str = "utf-8" protocol_version: int = 0 + full_protocol_version: int = 0 server_version: int = 0 backend_pid: int = 0 diff --git a/psycopg/psycopg/pq/_pq_ctypes.py b/psycopg/psycopg/pq/_pq_ctypes.py index 99e49357c..846b29559 100644 --- a/psycopg/psycopg/pq/_pq_ctypes.py +++ b/psycopg/psycopg/pq/_pq_ctypes.py @@ -39,6 +39,17 @@ if sys.platform == "linux": fdopen.argtypes = (c_int, c_char_p) fdopen.restype = FILE_ptr + +def not_supported_before(fname: str, pgversion: int) -> Any: + def not_supported(*args: Any, **kwargs: Any) -> NoReturn: + raise NotSupportedError( + f"{fname} requires libpq from PostgreSQL {version_pretty(pgversion)} on" + f" the client; version {version_pretty(libpq_version)} available instead" + ) + + return not_supported + + # Get the libpq version to define what functions are available. PQlibVersion = pq.PQlibVersion @@ -177,6 +188,13 @@ PQdb = pq.PQdb PQdb.argtypes = [PGconn_ptr] PQdb.restype = c_char_p +if libpq_version >= 180000: + PQservice = pq.PQservice + PQservice.argtypes = [PGconn_ptr] + PQservice.restype = c_char_p +else: + PQservice = not_supported_before("PQservice", 180000) + PQuser = pq.PQuser PQuser.argtypes = [PGconn_ptr] PQuser.restype = c_char_p @@ -190,16 +208,6 @@ PQhost.argtypes = [PGconn_ptr] PQhost.restype = c_char_p -def not_supported_before(fname: str, pgversion: int) -> Any: - def not_supported(*args: Any, **kwargs: Any) -> NoReturn: - raise NotSupportedError( - f"{fname} requires libpq from PostgreSQL {version_pretty(pgversion)} on" - f" the client; version {version_pretty(libpq_version)} available instead" - ) - - return not_supported - - if libpq_version >= 120000: PQhostaddr = pq.PQhostaddr PQhostaddr.argtypes = [PGconn_ptr] @@ -235,6 +243,13 @@ PQprotocolVersion = pq.PQprotocolVersion PQprotocolVersion.argtypes = [PGconn_ptr] PQprotocolVersion.restype = c_int +if libpq_version >= 180000: + PQfullProtocolVersion = pq.PQfullProtocolVersion + PQfullProtocolVersion.argtypes = [PGconn_ptr] + PQfullProtocolVersion.restype = c_int +else: + PQfullProtocolVersion = not_supported_before("PQfullProtocolVersion", 180000) + PQserverVersion = pq.PQserverVersion PQserverVersion.argtypes = [PGconn_ptr] PQserverVersion.restype = c_int diff --git a/psycopg/psycopg/pq/_pq_ctypes.pyi b/psycopg/psycopg/pq/_pq_ctypes.pyi index 091ef6be7..3936d08db 100644 --- a/psycopg/psycopg/pq/_pq_ctypes.pyi +++ b/psycopg/psycopg/pq/_pq_ctypes.pyi @@ -155,6 +155,7 @@ def PQresetStart(arg1: PGconn_struct | None) -> int: ... def PQresetPoll(arg1: PGconn_struct | None) -> int: ... def PQping(arg1: bytes) -> int: ... def PQdb(arg1: PGconn_struct | None) -> bytes | None: ... +def PQservice(arg1: PGconn_struct | None) -> bytes | None: ... def PQuser(arg1: PGconn_struct | None) -> bytes | None: ... def PQpass(arg1: PGconn_struct | None) -> bytes | None: ... def PQhost(arg1: PGconn_struct | None) -> bytes | None: ... @@ -165,6 +166,7 @@ def PQstatus(arg1: PGconn_struct | None) -> int: ... def PQtransactionStatus(arg1: PGconn_struct | None) -> int: ... def PQparameterStatus(arg1: PGconn_struct | None, arg2: bytes) -> bytes | None: ... def PQprotocolVersion(arg1: PGconn_struct | None) -> int: ... +def PQfullProtocolVersion(arg1: PGconn_struct | None) -> int: ... def PQserverVersion(arg1: PGconn_struct | None) -> int: ... def PQsocket(arg1: PGconn_struct | None) -> int: ... def PQbackendPID(arg1: PGconn_struct | None) -> int: ... diff --git a/psycopg/psycopg/pq/abc.py b/psycopg/psycopg/pq/abc.py index 5fbe112b9..c1ee596c1 100644 --- a/psycopg/psycopg/pq/abc.py +++ b/psycopg/psycopg/pq/abc.py @@ -48,6 +48,9 @@ class PGconn(Protocol): @property def db(self) -> bytes: ... + @property + def service(self) -> bytes: ... + @property def user(self) -> bytes: ... @@ -88,6 +91,9 @@ class PGconn(Protocol): @property def protocol_version(self) -> int: ... + @property + def full_protocol_version(self) -> int: ... + @property def server_version(self) -> int: ... diff --git a/psycopg/psycopg/pq/pq_ctypes.py b/psycopg/psycopg/pq/pq_ctypes.py index 7c16451e3..84a1dd559 100644 --- a/psycopg/psycopg/pq/pq_ctypes.py +++ b/psycopg/psycopg/pq/pq_ctypes.py @@ -177,6 +177,10 @@ class PGconn: def db(self) -> bytes: return self._call_bytes(impl.PQdb) + @property + def service(self) -> bytes: + return self._call_bytes(impl.PQservice) + @property def user(self) -> bytes: return self._call_bytes(impl.PQuser) @@ -236,6 +240,10 @@ class PGconn: def protocol_version(self) -> int: return self._call_int(impl.PQprotocolVersion) + @property + def full_protocol_version(self) -> int: + return self._call_int(impl.PQfullProtocolVersion) + @property def server_version(self) -> int: return self._call_int(impl.PQserverVersion) diff --git a/psycopg_c/psycopg_c/pq/libpq.pxd b/psycopg_c/psycopg_c/pq/libpq.pxd index 34840a29a..64f6600fd 100644 --- a/psycopg_c/psycopg_c/pq/libpq.pxd +++ b/psycopg_c/psycopg_c/pq/libpq.pxd @@ -128,6 +128,7 @@ cdef extern from "libpq-fe.h": # 33.2. Connection Status Functions char *PQdb(const PGconn *conn) + char *PQservice(const PGconn *conn) char *PQuser(const PGconn *conn) char *PQpass(const PGconn *conn) char *PQhost(const PGconn *conn) @@ -139,6 +140,7 @@ cdef extern from "libpq-fe.h": PGTransactionStatusType PQtransactionStatus(const PGconn *conn) const char *PQparameterStatus(const PGconn *conn, const char *paramName) int PQprotocolVersion(const PGconn *conn) + int PQfullProtocolVersion(const PGconn *conn) int PQserverVersion(const PGconn *conn) char *PQerrorMessage(const PGconn *conn) int PQsocket(const PGconn *conn) nogil @@ -362,4 +364,9 @@ typedef struct pg_cancel_conn PGcancelConn; #define PQcancelFinish(cancelConn) 0 #define PQsetChunkedRowsMode(conn, chunkSize) 0 #endif + +#if PG_VERSION_NUM < 180000 +#define PQfullProtocolVersion(conn) 0 +#define PQservice(conn) NULL +#endif """ diff --git a/psycopg_c/psycopg_c/pq/pgconn.pyx b/psycopg_c/psycopg_c/pq/pgconn.pyx index f8e23d4f3..590a51777 100644 --- a/psycopg_c/psycopg_c/pq/pgconn.pyx +++ b/psycopg_c/psycopg_c/pq/pgconn.pyx @@ -126,6 +126,13 @@ cdef class PGconn: def db(self) -> bytes: return _call_bytes(self, libpq.PQdb) + @property + def service(self) -> bytes: + _check_supported("PQservice", 180000) + _ensure_pgconn(self) + cdef char *rv = libpq.PQservice(self._pgconn_ptr) + return rv if rv is not NULL else b"" + @property def user(self) -> bytes: return _call_bytes(self, libpq.PQuser) @@ -196,6 +203,12 @@ cdef class PGconn: def protocol_version(self) -> int: return _call_int(self, libpq.PQprotocolVersion) + @property + def full_protocol_version(self) -> int: + _check_supported("PQfullProtocolVersion", 180000) + _ensure_pgconn(self) + return libpq.PQfullProtocolVersion(self._pgconn_ptr) + @property def server_version(self) -> int: return _call_int(self, libpq.PQserverVersion) diff --git a/tests/pq/test_pgconn.py b/tests/pq/test_pgconn.py index 75f74c463..4c86c591d 100644 --- a/tests/pq/test_pgconn.py +++ b/tests/pq/test_pgconn.py @@ -196,6 +196,17 @@ def test_db(pgconn): pgconn.db +@pytest.mark.libpq(">= 18") +def test_service(pgconn): + assert isinstance(pgconn.service, bytes) + + +@pytest.mark.libpq("< 18") +def test_service_notimpl(pgconn): + with pytest.raises(psycopg.NotSupportedError): + pgconn.service + + def test_user(pgconn): user = [o.val for o in pgconn.info if o.keyword == b"user"][0] assert pgconn.user == user @@ -306,6 +317,20 @@ def test_protocol_version(pgconn): pgconn.protocol_version +@pytest.mark.libpq(">= 18") +def test_full_protocol_version(pgconn): + assert pgconn.full_protocol_version >= 30000 + pgconn.finish() + with pytest.raises(psycopg.OperationalError): + pgconn.full_protocol_version + + +@pytest.mark.libpq("< 18") +def test_full_protocol_version_notimpl(pgconn): + with pytest.raises(psycopg.NotSupportedError): + pgconn.full_protocol_version + + def test_server_version(pgconn): assert pgconn.server_version >= 90400 pgconn.finish()