From 1c3f99c1e857afc6d14a5f7d957162ea63db0b11 Mon Sep 17 00:00:00 2001 From: Daniele Varrazzo Date: Sat, 5 Jul 2025 14:54:39 +0200 Subject: [PATCH] feat: add `ConnectionInfo.full_protocol_version`, `ConnectionInfo.service` Add matching capabilities methods too to allow checking if they are supported. --- docs/api/objects.rst | 22 ++++++++++++++++++++++ docs/news.rst | 4 ++++ psycopg/psycopg/_capabilities.py | 19 +++++++++++++++++++ psycopg/psycopg/_connection_info.py | 23 +++++++++++++++++++++++ tests/test_capabilities.py | 2 ++ tests/test_connection_info.py | 18 +++++++++++++++++- 6 files changed, 87 insertions(+), 1 deletion(-) diff --git a/docs/api/objects.rst b/docs/api/objects.rst index 5f6e2902e..c832f71cd 100644 --- a/docs/api/objects.rst +++ b/docs/api/objects.rst @@ -52,6 +52,10 @@ Connection information second group of digits is always 00. For example, version 9.3.5 is returned as 90305, version 10.2 as 100002. + .. autoattribute:: full_protocol_version + + .. versionadded:: 3.3 + .. autoattribute:: error_message .. automethod:: get_parameters @@ -80,6 +84,15 @@ Connection information .. autoattribute:: port .. autoattribute:: dbname + + .. autoattribute:: service + + Only available if the libpq used is from PostgreSQL 18 or newer. + Raise `~psycopg.NotSupportedError` otherwise. You can use the + `~Capabilities.has_service` capability to check for support. + + .. versionadded:: 3.3 + .. autoattribute:: user .. autoattribute:: password .. autoattribute:: options @@ -146,7 +159,16 @@ Libpq capabilities information .. versionadded:: 3.2 + .. automethod:: has_full_protocol_version + + .. versionadded:: 3.3 + .. automethod:: has_encrypt_password + + .. automethod:: has_service + + .. versionadded:: 3.3 + .. automethod:: has_hostaddr .. automethod:: has_pipeline .. automethod:: has_set_trace_flags diff --git a/docs/news.rst b/docs/news.rst index 3599cf298..3024cb23d 100644 --- a/docs/news.rst +++ b/docs/news.rst @@ -15,6 +15,10 @@ 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 `ConnectionInfo.full_protocol_version` and `~ConnectionInfo.service` + attributes (related to :ticket:`#1079`). +- Add `~Capabilities.has_service` and `~Capabilities.has_full_protocol_version` + capabilities (related to :ticket:`#1079`). - 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/_capabilities.py b/psycopg/psycopg/_capabilities.py index 323141efc..4596f0cf4 100644 --- a/psycopg/psycopg/_capabilities.py +++ b/psycopg/psycopg/_capabilities.py @@ -18,6 +18,18 @@ class Capabilities: def __init__(self) -> None: self._cache: dict[str, str] = {} + def has_full_protocol_version(self, check: bool = False) -> bool: + """Check if the `PGconn.full_protocol_version()` method is implemented. + + If the method is implemented, then `ConnectionInfo.full_protocol_version` + will return a meaningful value. + + The feature requires libpq 18.0 and greater. + """ + return self._has_feature( + "pq.PGconn.full_protocol_version()", 180000, check=check + ) + def has_encrypt_password(self, check: bool = False) -> bool: """Check if the `PGconn.encrypt_password()` method is implemented. @@ -25,6 +37,13 @@ class Capabilities: """ return self._has_feature("pq.PGconn.encrypt_password()", 100000, check=check) + def has_service(self, check: bool = False) -> bool: + """Check if the `ConnectionInfo.service` attribute is implemented. + + The feature requires libpq 18.0 and greater. + """ + return self._has_feature("Connection.info.service", 180000, check=check) + def has_hostaddr(self, check: bool = False) -> bool: """Check if the `ConnectionInfo.hostaddr` attribute is implemented. diff --git a/psycopg/psycopg/_connection_info.py b/psycopg/psycopg/_connection_info.py index 84554c6f9..572062faf 100644 --- a/psycopg/psycopg/_connection_info.py +++ b/psycopg/psycopg/_connection_info.py @@ -57,6 +57,11 @@ class ConnectionInfo: """The database name of the connection. See :pq:`PQdb()`.""" return self._get_pgconn_attr("db") + @property + def service(self) -> str: + """The service name of the connection. See :pq:`PQservice()`.""" + return self._get_pgconn_attr("service") + @property def user(self) -> str: """The user name of the connection. See :pq:`PQuser()`.""" @@ -152,6 +157,24 @@ class ConnectionInfo: """ return self.pgconn.server_version + @property + def full_protocol_version(self) -> int: + """ + An integer representing the server full protocol version. + + Return a value in the format described in :pq:`PQfullProtocolVersion()`. + + Only meaningful if the libpq used is version 18 or greater. If the + version is lesser than that, return the value reported by + :pq:`PQprotocolVersion()` (but in the same format as above, e.g. 30000 + for version 3). You can use `Capabilities.has_full_protocol_version()` + to verify if the value can be considered reliable. + """ + try: + return self.pgconn.full_protocol_version + except e.NotSupportedError: + return self.pgconn.protocol_version * 10000 + @property def backend_pid(self) -> int: """ diff --git a/tests/test_capabilities.py b/tests/test_capabilities.py index 674a083c2..392a0c4a1 100644 --- a/tests/test_capabilities.py +++ b/tests/test_capabilities.py @@ -11,7 +11,9 @@ except ImportError: pass caps = [ + ("has_full_protocol_version", "pq.PGconn.full_protocol_version()", 18), ("has_encrypt_password", "pq.PGconn.encrypt_password()", 10), + ("has_service", "Connection.info.service", 18), ("has_hostaddr", "Connection.info.hostaddr", 12), ("has_pipeline", "Connection.pipeline()", 14), ("has_set_trace_flags", "PGconn.set_trace_flags()", 14), diff --git a/tests/test_connection_info.py b/tests/test_connection_info.py index 2ff96e06c..d339255a5 100644 --- a/tests/test_connection_info.py +++ b/tests/test_connection_info.py @@ -11,7 +11,7 @@ from .fix_crdb import crdb_encoding @pytest.mark.parametrize( "attr", - [("dbname", "db"), "host", "hostaddr", "user", "password", "options"], + [("dbname", "db"), "service", "host", "hostaddr", "user", "password", "options"], ) def test_attrs(conn, attr): if isinstance(attr, tuple): @@ -22,6 +22,9 @@ def test_attrs(conn, attr): if info_attr == "hostaddr" and psycopg.pq.version() < 120000: pytest.skip("hostaddr not supported on libpq < 12") + if info_attr == "service" and psycopg.pq.version() < 180000: + pytest.skip("service not supported on libpq < 18") + info_val = getattr(conn.info, info_attr) pgconn_val = getattr(conn.pgconn, pgconn_attr).decode() assert info_val == pgconn_val @@ -31,6 +34,12 @@ def test_attrs(conn, attr): getattr(conn.info, info_attr) +@pytest.mark.libpq("< 18") +def test_service_not_supported(conn): + with pytest.raises(psycopg.NotSupportedError): + conn.info.service + + @pytest.mark.libpq("< 12") def test_hostaddr_not_supported(conn): with pytest.raises(psycopg.NotSupportedError): @@ -44,6 +53,13 @@ def test_port(conn): conn.info.port +def test_full_protocol_version(conn): + assert conn.info.full_protocol_version >= 30000 + conn.close() + with pytest.raises(psycopg.OperationalError): + conn.info.full_protocol_version + + @pytest.mark.skipif(psycopg.pq.__impl__ != "python", reason="can't monkeypatch C") def test_blank_port(conn, monkeypatch): -- 2.47.2