]> git.ipfire.org Git - thirdparty/psycopg.git/commitdiff
feat: add PGconn.full_protocol_version, PGconn.service attributes
authorDaniele Varrazzo <daniele.varrazzo@gmail.com>
Sat, 5 Jul 2025 11:50:02 +0000 (13:50 +0200)
committerDaniele Varrazzo <daniele.varrazzo@gmail.com>
Sat, 5 Jul 2025 14:10:08 +0000 (16:10 +0200)
Wrap libpq 18 PQfullProtocolVersion and PQservice functions.

See #1079

docs/conf.py
docs/news.rst
psycopg/psycopg/errors.py
psycopg/psycopg/pq/_pq_ctypes.py
psycopg/psycopg/pq/_pq_ctypes.pyi
psycopg/psycopg/pq/abc.py
psycopg/psycopg/pq/pq_ctypes.py
psycopg_c/psycopg_c/pq/libpq.pxd
psycopg_c/psycopg_c/pq/pgconn.pyx
tests/pq/test_pgconn.py

index cf87980d32f32a01eba0359c8a9678e315219adb..0d4e77dee5939612a37508e4bcf95ff3612f9cb9 100644 (file)
@@ -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"
index a423a1dfcb4d4a320d96fde09f1f773826e6644b..3599cf2988d112bcaaa8f7dc2431467ad627f29b 100644 (file)
@@ -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`).
 
 
index f5e0396d143461949df81002971b2bacb3bc726d..63deca1b9ffadf8a1e1c62ca0057b8b767c49f5b 100644 (file)
@@ -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
index 99e49357c7c59d66e0958167754350c0fc07e492..846b29559d909da78c88445b36c6241e91e3888c 100644 (file)
@@ -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
index 091ef6be7335202c76aea23bb2ac192d30ce6826..3936d08db53fcdb26a5964edc98d893468f5ade1 100644 (file)
@@ -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: ...
index 5fbe112b95ca752ab57f27a23556bae48ff83191..c1ee596c1ea03862bfd01a26ceb77a4550a8dee7 100644 (file)
@@ -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: ...
 
index 7c16451e3cf1b9796a93f8a84b5f3658ca90ed94..84a1dd559db398c30bcde9c7abc32e0078d1a439 100644 (file)
@@ -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)
index 34840a29a391c0ec6c65d1852d47f891ecd0a9f0..64f6600fd399f5adb39be5bcf064abe1bc61be37 100644 (file)
@@ -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
 """
index f8e23d4f3397f5822cfdafffb282c3ebf05ee70e..590a5177789201ad148efc788f2b1b621533d540 100644 (file)
@@ -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)
index 75f74c46370c2d27aac7c623a8a62f721821fb59..4c86c591d7a293630e08e3830285b9c9aac06318 100644 (file)
@@ -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()