]> git.ipfire.org Git - thirdparty/psycopg.git/commitdiff
feat: add get_error_message() methods
authorDaniele Varrazzo <daniele.varrazzo@gmail.com>
Wed, 12 Jun 2024 10:38:04 +0000 (12:38 +0200)
committerDaniele Varrazzo <daniele.varrazzo@gmail.com>
Thu, 13 Jun 2024 14:04:25 +0000 (16:04 +0200)
The method is available on PGconn, PGconnCancel, PGresult.

The generic function error_message() is retained only because it was
documented so we don't want to drop it. Internally only the
get_error_message() method is used.

26 files changed:
psycopg/psycopg/_connection_base.py
psycopg/psycopg/_connection_info.py
psycopg/psycopg/_copy.py
psycopg/psycopg/_copy_async.py
psycopg/psycopg/_copy_base.py
psycopg/psycopg/_cursor_base.py
psycopg/psycopg/_encodings.py
psycopg/psycopg/_pipeline.py
psycopg/psycopg/connection.py
psycopg/psycopg/connection_async.py
psycopg/psycopg/errors.py
psycopg/psycopg/generators.py
psycopg/psycopg/pq/abc.py
psycopg/psycopg/pq/misc.py
psycopg/psycopg/pq/pq_ctypes.py
psycopg_c/psycopg_c/_psycopg/adapt.pyx
psycopg_c/psycopg_c/_psycopg/generators.pyx
psycopg_c/psycopg_c/pq.pyx
psycopg_c/psycopg_c/pq/escaping.pyx
psycopg_c/psycopg_c/pq/pgcancel.pyx
psycopg_c/psycopg_c/pq/pgconn.pyx
psycopg_c/psycopg_c/pq/pgresult.pyx
tests/fix_db.py
tests/pq/test_misc.py
tests/pq/test_pgconn.py
tests/pq/test_pgresult.py

index 8a9a6e50def49266f296ba4048334784ded3cb17..091dc6826efc2cf7ac240da88edaae8183535eff 100644 (file)
@@ -26,7 +26,6 @@ from ._enums import IsolationLevel
 from ._compat import LiteralString, Self, TypeAlias, TypeVar
 from .pq.misc import connection_summary
 from ._pipeline import BasePipeline
-from ._encodings import pgconn_encoding
 from ._preparing import PrepareManager
 from ._connection_info import ConnectionInfo
 
@@ -343,7 +342,7 @@ class BaseConnection(Generic[Row]):
         if not (self and self._notice_handlers):
             return
 
-        diag = e.Diagnostic(res, pgconn_encoding(self.pgconn))
+        diag = e.Diagnostic(res, self.pgconn._encoding)
         for cb in self._notice_handlers:
             try:
                 cb(diag)
@@ -376,7 +375,7 @@ class BaseConnection(Generic[Row]):
         if not (self and self._notify_handlers):
             return
 
-        enc = pgconn_encoding(self.pgconn)
+        enc = self.pgconn._encoding
         n = Notify(pgn.relname.decode(enc), pgn.extra.decode(enc), pgn.be_pid)
         for cb in self._notify_handlers:
             cb(n)
@@ -447,7 +446,7 @@ class BaseConnection(Generic[Row]):
         self._check_connection_ok()
 
         if isinstance(command, str):
-            command = command.encode(pgconn_encoding(self.pgconn))
+            command = command.encode(self.pgconn._encoding)
         elif isinstance(command, Composable):
             command = command.as_bytes(self)
 
@@ -473,7 +472,7 @@ class BaseConnection(Generic[Row]):
         result = (yield from generators.execute(self.pgconn))[-1]
         if result.status != COMMAND_OK and result.status != TUPLES_OK:
             if result.status == FATAL_ERROR:
-                raise e.error_from_result(result, encoding=pgconn_encoding(self.pgconn))
+                raise e.error_from_result(result, encoding=self.pgconn._encoding)
             else:
                 raise e.InterfaceError(
                     f"unexpected result {pq.ExecStatus(result.status).name}"
@@ -513,7 +512,7 @@ class BaseConnection(Generic[Row]):
         result = (yield from generators.execute(self.pgconn))[-1]
         if result.status != COMMAND_OK:
             if result.status == FATAL_ERROR:
-                raise e.error_from_result(result, encoding=pgconn_encoding(self.pgconn))
+                raise e.error_from_result(result, encoding=self.pgconn._encoding)
             else:
                 raise e.InterfaceError(
                     f"unexpected result {pq.ExecStatus(result.status).name}"
index 79a781cb09d1ee7118acd782c7620aec76926b95..0db591310c25bda6261d2bd97a7ffdc0367b26a6 100644 (file)
@@ -11,7 +11,6 @@ from datetime import tzinfo
 
 from . import pq
 from ._tz import get_tzinfo
-from ._encodings import pgconn_encoding
 from .conninfo import make_conninfo
 
 
@@ -167,7 +166,7 @@ class ConnectionInfo:
     @property
     def encoding(self) -> str:
         """The Python codec name of the connection's client encoding."""
-        return pgconn_encoding(self.pgconn)
+        return self.pgconn._encoding
 
     def _get_pgconn_attr(self, name: str) -> str:
         value: bytes = getattr(self.pgconn, name)
index 1c393a9d632925f4ab4079930a3683d0b071bfef..6d85066d89e7cadd997714828ef1f4fe2ab4494d 100644 (file)
@@ -18,7 +18,6 @@ from . import errors as e
 from ._compat import Self
 from ._copy_base import BaseCopy, MAX_BUFFER_SIZE, QUEUE_SIZE, PREFER_FLUSH
 from .generators import copy_to, copy_end
-from ._encodings import pgconn_encoding
 from ._acompat import spawn, gather, Queue, Worker
 
 if TYPE_CHECKING:
@@ -214,7 +213,7 @@ class LibpqWriter(Writer):
         bmsg: bytes | None
         if exc:
             msg = f"error from Python: {type(exc).__qualname__} - {exc}"
-            bmsg = msg.encode(pgconn_encoding(self._pgconn), "replace")
+            bmsg = msg.encode(self._pgconn._encoding, "replace")
         else:
             bmsg = None
 
index 2c630da9b96d7b12d7c56d46f660e4de41b521dd..02d27bae3e7b760f9ece18bd97c17ab18bcf7a46 100644 (file)
@@ -15,7 +15,6 @@ from . import errors as e
 from ._compat import Self
 from ._copy_base import BaseCopy, MAX_BUFFER_SIZE, QUEUE_SIZE, PREFER_FLUSH
 from .generators import copy_to, copy_end
-from ._encodings import pgconn_encoding
 from ._acompat import aspawn, agather, AQueue, AWorker
 
 if TYPE_CHECKING:
@@ -213,7 +212,7 @@ class AsyncLibpqWriter(AsyncWriter):
         bmsg: bytes | None
         if exc:
             msg = f"error from Python: {type(exc).__qualname__} - {exc}"
-            bmsg = msg.encode(pgconn_encoding(self._pgconn), "replace")
+            bmsg = msg.encode(self._pgconn._encoding, "replace")
         else:
             bmsg = None
 
index d3a143c5f5cc8167eea39d6a99b612eee7f332d9..9217a90e1e80878b4e8b36fbb3f210b296b7ee43 100644 (file)
@@ -18,7 +18,6 @@ from . import errors as e
 from .abc import Buffer, ConnectionType, PQGen, Transformer
 from .pq.misc import connection_summary
 from ._cmodule import _psycopg
-from ._encodings import pgconn_encoding
 from .generators import copy_from
 
 if TYPE_CHECKING:
@@ -101,7 +100,7 @@ class BaseCopy(Generic[ConnectionType]):
         if binary:
             self.formatter = BinaryFormatter(tx)
         else:
-            self.formatter = TextFormatter(tx, encoding=pgconn_encoding(self._pgconn))
+            self.formatter = TextFormatter(tx, encoding=self._pgconn._encoding)
 
         self._finished = False
 
index 1f223451339f6dbff58d153000a19fcc167c5395..9448ec505d3a2cb1ddb5bb34b680c0a1c0d8ca44 100644 (file)
@@ -18,7 +18,6 @@ from .rows import Row, RowMaker
 from ._column import Column
 from .pq.misc import connection_summary
 from ._queries import PostgresQuery, PostgresClientQuery
-from ._encodings import pgconn_encoding
 from ._preparing import Prepare
 from .generators import execute, fetch, send
 
@@ -615,4 +614,4 @@ class BaseCursor(Generic[ConnectionType, Row]):
 
     @property
     def _encoding(self) -> str:
-        return pgconn_encoding(self._pgconn)
+        return self._pgconn._encoding
index b515f96fd734b71b8aa8bb6b7b62b5d6f6504b47..42da2c529088f5272b6b8c82929d644411293e32 100644 (file)
@@ -16,7 +16,6 @@ from .errors import NotSupportedError
 from ._compat import cache
 
 if TYPE_CHECKING:
-    from .pq.abc import PGconn
     from ._connection_base import BaseConnection
 
 OK = ConnStatus.OK
@@ -86,23 +85,7 @@ def conn_encoding(conn: BaseConnection[Any] | None) -> str:
 
     Default to utf8 if the connection has no encoding info.
     """
-    if conn:
-        return pgconn_encoding(conn.pgconn)
-    else:
-        return "utf-8"
-
-
-def pgconn_encoding(pgconn: PGconn) -> str:
-    """
-    Return the Python encoding name of a libpq connection.
-
-    Default to utf8 if the connection has no encoding info.
-    """
-    if pgconn.status == OK:
-        pgenc = pgconn.parameter_status(b"client_encoding") or b"UTF8"
-        return pg2pyenc(pgenc)
-    else:
-        return "utf-8"
+    return conn.pgconn._encoding if conn else "utf-8"
 
 
 def conninfo_encoding(conninfo: str) -> str:
index 441b2147c7a0f7e4db8cce7a4abf52a9f646d5a3..6ad3b303fe9ad672ca574ac17f2b2007772a4b43 100644 (file)
@@ -15,7 +15,6 @@ from . import errors as e
 from .abc import PipelineCommand, PQGen
 from ._compat import Deque, Self, TypeAlias
 from .pq.misc import connection_summary
-from ._encodings import pgconn_encoding
 from .generators import pipeline_communicate, fetch_many, send
 from ._capabilities import capabilities
 
@@ -168,7 +167,7 @@ class BasePipeline:
         if queued is None:
             (result,) = results
             if result.status == FATAL_ERROR:
-                raise e.error_from_result(result, encoding=pgconn_encoding(self.pgconn))
+                raise e.error_from_result(result, encoding=self.pgconn._encoding)
             elif result.status == PIPELINE_ABORTED:
                 raise e.PipelineAborted("pipeline aborted")
         else:
index 277d3ab7cd57bd89e41d778af396c6b7bd78a99b..8051a1b5cffa9875725679090afd20711f0bce30 100644 (file)
@@ -27,7 +27,6 @@ from ._compat import Self
 from .conninfo import make_conninfo, conninfo_to_dict
 from .conninfo import conninfo_attempts, timeout_from_conninfo
 from ._pipeline import Pipeline
-from ._encodings import pgconn_encoding
 from .generators import notifies
 from .transaction import Transaction
 from .cursor import Cursor
@@ -344,7 +343,7 @@ class Connection(BaseConnection[Row]):
                 with self.lock:
                     ns = self.wait(notifies(self.pgconn), interval=interval)
                     if ns:
-                        enc = pgconn_encoding(self.pgconn)
+                        enc = self.pgconn._encoding
             except e._NO_TRACEBACK as ex:
                 raise ex.with_traceback(None)
 
index d66f6fbf3f1567434c3887fb2913850d9cc4cda6..8252594b5ac391acc39ea7fad8aaa81afe0abf98 100644 (file)
@@ -24,7 +24,6 @@ from ._compat import Self
 from .conninfo import make_conninfo, conninfo_to_dict
 from .conninfo import conninfo_attempts_async, timeout_from_conninfo
 from ._pipeline import AsyncPipeline
-from ._encodings import pgconn_encoding
 from .generators import notifies
 from .transaction import AsyncTransaction
 from .cursor_async import AsyncCursor
@@ -364,7 +363,7 @@ class AsyncConnection(BaseConnection[Row]):
                 async with self.lock:
                     ns = await self.wait(notifies(self.pgconn), interval=interval)
                     if ns:
-                        enc = pgconn_encoding(self.pgconn)
+                        enc = self.pgconn._encoding
             except e._NO_TRACEBACK as ex:
                 raise ex.with_traceback(None)
 
index 00cb24c9c9455c28ff3b35c5ed3a425bfa838061..ed6c238143e8741225a915aadd447a185c8f87f5 100644 (file)
@@ -59,6 +59,7 @@ class FinishedPGconn:
     pipeline_status: int = PipelineStatus.OFF.value
 
     error_message: bytes = b""
+    _encoding: str = "utf-8"
     server_version: int = 0
 
     backend_pid: int = 0
@@ -92,6 +93,9 @@ class FinishedPGconn:
     def reset(self) -> NoReturn:
         self._raise()
 
+    def get_error_message(self, encoding: str = "") -> str:
+        return "the conenection is closed"
+
     def reset_start(self) -> NoReturn:
         self._raise()
 
@@ -544,15 +548,9 @@ def lookup(sqlstate: str) -> type[Error]:
 
 
 def error_from_result(result: PGresult, encoding: str = "utf-8") -> Error:
-    from psycopg import pq
-
     state = result.error_field(DiagnosticField.SQLSTATE) or b""
-    cls = _class_for_state(state.decode("ascii"))
-    return cls(
-        pq.error_message(result, encoding=encoding),
-        info=result,
-        encoding=encoding,
-    )
+    cls = _class_for_state(state.decode("utf-8", "replace"))
+    return cls(result.get_error_message(encoding), info=result, encoding=encoding)
 
 
 def _is_pgresult(info: ErrorInfo) -> TypeGuard[PGresult]:
index caafde0da20d16194197ba7a05890904d583bae8..682d7ce4e991126c775ec5ad720bf9795680d7b4 100644 (file)
@@ -32,7 +32,7 @@ from .pq.abc import PGcancelConn, PGconn, PGresult
 from .waiting import Wait, Ready
 from ._compat import Deque
 from ._cmodule import _psycopg
-from ._encodings import pgconn_encoding, conninfo_encoding
+from ._encodings import conninfo_encoding
 
 OK = pq.ConnStatus.OK
 BAD = pq.ConnStatus.BAD
@@ -69,7 +69,7 @@ def _connect(conninfo: str, *, timeout: float = 0.0) -> PQGenConn[PGconn]:
         if conn.status == BAD:
             encoding = conninfo_encoding(conninfo)
             raise e.OperationalError(
-                f"connection is bad: {pq.error_message(conn, encoding=encoding)}",
+                f"connection is bad: {conn.get_error_message(encoding)}",
                 pgconn=conn,
             )
 
@@ -89,7 +89,7 @@ def _connect(conninfo: str, *, timeout: float = 0.0) -> PQGenConn[PGconn]:
         elif status == POLL_FAILED:
             encoding = conninfo_encoding(conninfo)
             raise e.OperationalError(
-                f"connection failed: {pq.error_message(conn, encoding=encoding)}",
+                f"connection failed: {conn.get_error_message(encoding)}",
                 pgconn=e.finish_pgconn(conn),
             )
         else:
@@ -114,8 +114,9 @@ def _cancel(cancel_conn: PGcancelConn, *, timeout: float = 0.0) -> PQGenConn[Non
         elif status == POLL_WRITING:
             yield cancel_conn.socket, WAIT_W
         elif status == POLL_FAILED:
-            msg = cancel_conn.error_message.decode("utf8", "replace")
-            raise e.OperationalError(f"cancellation failed: {msg}")
+            raise e.OperationalError(
+                f"cancellation failed: {cancel_conn.get_error_message()}"
+            )
         else:
             raise e.InternalError(f"unexpected poll status: {status}")
 
@@ -327,8 +328,7 @@ def copy_from(pgconn: PGconn) -> PQGen[memoryview | PGresult]:
         raise e.ProgrammingError("you cannot mix COPY with other operations")
     result = results[0]
     if result.status != COMMAND_OK:
-        encoding = pgconn_encoding(pgconn)
-        raise e.error_from_result(result, encoding=encoding)
+        raise e.error_from_result(result, encoding=pgconn._encoding)
 
     return result
 
@@ -382,8 +382,7 @@ def copy_end(pgconn: PGconn, error: bytes | None) -> PQGen[PGresult]:
     # Retrieve the final result of copy
     (result,) = yield from _fetch_many(pgconn)
     if result.status != COMMAND_OK:
-        encoding = pgconn_encoding(pgconn)
-        raise e.error_from_result(result, encoding=encoding)
+        raise e.error_from_result(result, encoding=pgconn._encoding)
 
     return result
 
index ed0acb4085e7c19e8e55fdf8548fefa530a1e795..e7d617f38f2cf76b0808d86305690cf1b2b0f43f 100644 (file)
@@ -79,6 +79,11 @@ class PGconn(Protocol):
     @property
     def error_message(self) -> bytes: ...
 
+    def get_error_message(self, encoding: str = ...) -> str: ...
+
+    @property
+    def _encoding(self) -> str: ...
+
     @property
     def server_version(self) -> int: ...
 
@@ -230,6 +235,8 @@ class PGresult(Protocol):
     @property
     def error_message(self) -> bytes: ...
 
+    def get_error_message(self, encoding: str = ...) -> str: ...
+
     def error_field(self, fieldcode: int) -> bytes | None: ...
 
     @property
@@ -290,6 +297,8 @@ class PGcancelConn(Protocol):
     @property
     def error_message(self) -> bytes: ...
 
+    def get_error_message(self, encoding: str = ...) -> str: ...
+
     def reset(self) -> None: ...
 
     def finish(self) -> None: ...
index 6a5294ba0798be313989b501602e7f7a3c07b930..d5ab483e936cf707f61308068352398dfb2a1bbd 100644 (file)
@@ -11,12 +11,11 @@ import os
 import sys
 import logging
 import ctypes.util
-from typing import cast, NamedTuple
+from typing import NamedTuple
 
-from .abc import PGconn, PGresult
+from . import abc
 from ._enums import ConnStatus, TransactionStatus, PipelineStatus
 from .._compat import cache
-from .._encodings import pgconn_encoding
 
 logger = logging.getLogger("psycopg.pq")
 
@@ -76,38 +75,23 @@ def find_libpq_full_path() -> str | None:
     return libname
 
 
-def error_message(obj: PGconn | PGresult, encoding: str = "utf8") -> str:
+def error_message(
+    obj: abc.PGconn | abc.PGresult | abc.PGcancelConn, encoding: str = ""
+) -> str:
     """
-    Return an error message from a `PGconn` or `PGresult`.
+    Return an error message from a `PGconn`, `PGresult`, `PGcancelConn`.
 
     The return value is a `!str` (unlike pq data which is usually `!bytes`):
     use the connection encoding if available, otherwise the `!encoding`
     parameter as a fallback for decoding. Don't raise exceptions on decoding
     errors.
-
     """
-    bmsg: bytes
-
-    if hasattr(obj, "error_field"):
-        # obj is a PGresult
-        obj = cast(PGresult, obj)
-        bmsg = obj.error_message
-
-    elif hasattr(obj, "error_message"):
-        # obj is a PGconn
-        if obj.status == OK:
-            encoding = pgconn_encoding(obj)
-        bmsg = obj.error_message
-
-    else:
-        raise TypeError(f"PGconn or PGresult expected, got {type(obj).__name__}")
+    # Note: this function is exposed by the pq module and was documented, therefore
+    # we are not going to remove it, but we don't use it internally.
 
-    if bmsg:
-        msg = strip_severity(bmsg.decode(encoding, "replace"))
-    else:
-        msg = "no details available"
-
-    return msg
+    # Don't pass the encoding if not specified, because different classes have
+    # different defaults (conn has its own encoding. others default to utf8).
+    return obj.get_error_message(encoding) if encoding else obj.get_error_message()
 
 
 # Possible prefixes to strip for error messages, in the known localizations.
@@ -148,7 +132,15 @@ def strip_severity(msg: str) -> str:
     return msg.strip()
 
 
-def connection_summary(pgconn: PGconn) -> str:
+def _clean_error_message(msg: bytes, encoding: str) -> str:
+    smsg = msg.decode(encoding, "replace")
+    if smsg:
+        return strip_severity(smsg)
+    else:
+        return "no error details available"
+
+
+def connection_summary(pgconn: abc.PGconn) -> str:
     """
     Return summary information on a connection.
 
index da4f026a7864ab0fbbd794f207d1b253dacd2dcf..8335f87f7ffecd3aa038573439736979432227fe 100644 (file)
@@ -21,10 +21,11 @@ from typing import Any, Callable, Sequence
 from typing import cast as t_cast, TYPE_CHECKING
 
 from .. import errors as e
+from .._encodings import pg2pyenc
 from . import _pq_ctypes as impl
 from .misc import PGnotify, ConninfoOption, PGresAttDesc
-from .misc import error_message, connection_summary
-from ._enums import Format, ExecStatus, Trace
+from .misc import connection_summary, _clean_error_message
+from ._enums import ConnStatus, ExecStatus, Format, Trace
 
 # Imported locally to call them from __del__ methods
 from ._pq_ctypes import PQclear, PQfinish, PQfreeCancel, PQcancelFinish, PQstatus
@@ -36,6 +37,8 @@ __impl__ = "python"
 
 logger = logging.getLogger("psycopg")
 
+OK = ConnStatus.OK
+
 
 def version() -> int:
     """Return the version number of the libpq currently loaded.
@@ -221,6 +224,17 @@ class PGconn:
     def error_message(self) -> bytes:
         return impl.PQerrorMessage(self._pgconn_ptr)
 
+    def get_error_message(self, encoding: str = "") -> str:
+        return _clean_error_message(self.error_message, encoding or self._encoding)
+
+    @property
+    def _encoding(self) -> str:
+        if self.status == OK:
+            pgenc = self.parameter_status(b"client_encoding") or b"UTF8"
+            return pg2pyenc(pgenc)
+        else:
+            return "utf-8"
+
     @property
     def protocol_version(self) -> int:
         return self._call_int(impl.PQprotocolVersion)
@@ -267,7 +281,9 @@ class PGconn:
         self._ensure_pgconn()
         rv = impl.PQexec(self._pgconn_ptr, command)
         if not rv:
-            raise e.OperationalError(f"executing query failed: {error_message(self)}")
+            raise e.OperationalError(
+                f"executing query failed: {self.get_error_message()}"
+            )
         return PGresult(rv)
 
     def send_query(self, command: bytes) -> None:
@@ -275,7 +291,9 @@ class PGconn:
             raise TypeError(f"bytes expected, got {type(command)} instead")
         self._ensure_pgconn()
         if not impl.PQsendQuery(self._pgconn_ptr, command):
-            raise e.OperationalError(f"sending query failed: {error_message(self)}")
+            raise e.OperationalError(
+                f"sending query failed: {self.get_error_message()}"
+            )
 
     def exec_params(
         self,
@@ -291,7 +309,9 @@ class PGconn:
         self._ensure_pgconn()
         rv = impl.PQexecParams(*args)
         if not rv:
-            raise e.OperationalError(f"executing query failed: {error_message(self)}")
+            raise e.OperationalError(
+                f"executing query failed: {self.get_error_message()}"
+            )
         return PGresult(rv)
 
     def send_query_params(
@@ -308,7 +328,7 @@ class PGconn:
         self._ensure_pgconn()
         if not impl.PQsendQueryParams(*args):
             raise e.OperationalError(
-                f"sending query and params failed: {error_message(self)}"
+                f"sending query and params failed: {self.get_error_message()}"
             )
 
     def send_prepare(
@@ -328,7 +348,7 @@ class PGconn:
         self._ensure_pgconn()
         if not impl.PQsendPrepare(self._pgconn_ptr, name, command, nparams, atypes):
             raise e.OperationalError(
-                f"sending query and params failed: {error_message(self)}"
+                f"sending query and params failed: {self.get_error_message()}"
             )
 
     def send_query_prepared(
@@ -348,7 +368,7 @@ class PGconn:
         self._ensure_pgconn()
         if not impl.PQsendQueryPrepared(*args):
             raise e.OperationalError(
-                f"sending prepared query failed: {error_message(self)}"
+                f"sending prepared query failed: {self.get_error_message()}"
             )
 
     def _query_params_args(
@@ -432,7 +452,9 @@ class PGconn:
         self._ensure_pgconn()
         rv = impl.PQprepare(self._pgconn_ptr, name, command, nparams, atypes)
         if not rv:
-            raise e.OperationalError(f"preparing query failed: {error_message(self)}")
+            raise e.OperationalError(
+                f"preparing query failed: {self.get_error_message()}"
+            )
         return PGresult(rv)
 
     def exec_prepared(
@@ -483,7 +505,7 @@ class PGconn:
         )
         if not rv:
             raise e.OperationalError(
-                f"executing prepared query failed: {error_message(self)}"
+                f"executing prepared query failed: {self.get_error_message()}"
             )
         return PGresult(rv)
 
@@ -493,7 +515,9 @@ class PGconn:
         self._ensure_pgconn()
         rv = impl.PQdescribePrepared(self._pgconn_ptr, name)
         if not rv:
-            raise e.OperationalError(f"describe prepared failed: {error_message(self)}")
+            raise e.OperationalError(
+                f"describe prepared failed: {self.get_error_message()}"
+            )
         return PGresult(rv)
 
     def send_describe_prepared(self, name: bytes) -> None:
@@ -502,7 +526,7 @@ class PGconn:
         self._ensure_pgconn()
         if not impl.PQsendDescribePrepared(self._pgconn_ptr, name):
             raise e.OperationalError(
-                f"sending describe prepared failed: {error_message(self)}"
+                f"sending describe prepared failed: {self.get_error_message()}"
             )
 
     def describe_portal(self, name: bytes) -> PGresult:
@@ -511,7 +535,9 @@ class PGconn:
         self._ensure_pgconn()
         rv = impl.PQdescribePortal(self._pgconn_ptr, name)
         if not rv:
-            raise e.OperationalError(f"describe portal failed: {error_message(self)}")
+            raise e.OperationalError(
+                f"describe portal failed: {self.get_error_message()}"
+            )
         return PGresult(rv)
 
     def send_describe_portal(self, name: bytes) -> None:
@@ -520,7 +546,7 @@ class PGconn:
         self._ensure_pgconn()
         if not impl.PQsendDescribePortal(self._pgconn_ptr, name):
             raise e.OperationalError(
-                f"sending describe portal failed: {error_message(self)}"
+                f"sending describe portal failed: {self.get_error_message()}"
             )
 
     def close_prepared(self, name: bytes) -> PGresult:
@@ -529,7 +555,9 @@ class PGconn:
         self._ensure_pgconn()
         rv = impl.PQclosePrepared(self._pgconn_ptr, name)
         if not rv:
-            raise e.OperationalError(f"close prepared failed: {error_message(self)}")
+            raise e.OperationalError(
+                f"close prepared failed: {self.get_error_message()}"
+            )
         return PGresult(rv)
 
     def send_close_prepared(self, name: bytes) -> None:
@@ -538,7 +566,7 @@ class PGconn:
         self._ensure_pgconn()
         if not impl.PQsendClosePrepared(self._pgconn_ptr, name):
             raise e.OperationalError(
-                f"sending close prepared failed: {error_message(self)}"
+                f"sending close prepared failed: {self.get_error_message()}"
             )
 
     def close_portal(self, name: bytes) -> PGresult:
@@ -547,7 +575,7 @@ class PGconn:
         self._ensure_pgconn()
         rv = impl.PQclosePortal(self._pgconn_ptr, name)
         if not rv:
-            raise e.OperationalError(f"close portal failed: {error_message(self)}")
+            raise e.OperationalError(f"close portal failed: {self.get_error_message()}")
         return PGresult(rv)
 
     def send_close_portal(self, name: bytes) -> None:
@@ -556,7 +584,7 @@ class PGconn:
         self._ensure_pgconn()
         if not impl.PQsendClosePortal(self._pgconn_ptr, name):
             raise e.OperationalError(
-                f"sending close portal failed: {error_message(self)}"
+                f"sending close portal failed: {self.get_error_message()}"
             )
 
     def get_result(self) -> PGresult | None:
@@ -565,7 +593,9 @@ class PGconn:
 
     def consume_input(self) -> None:
         if 1 != impl.PQconsumeInput(self._pgconn_ptr):
-            raise e.OperationalError(f"consuming input failed: {error_message(self)}")
+            raise e.OperationalError(
+                f"consuming input failed: {self.get_error_message()}"
+            )
 
     def is_busy(self) -> int:
         return impl.PQisBusy(self._pgconn_ptr)
@@ -578,7 +608,7 @@ class PGconn:
     def nonblocking(self, arg: int) -> None:
         if 0 > impl.PQsetnonblocking(self._pgconn_ptr, arg):
             raise e.OperationalError(
-                f"setting nonblocking failed: {error_message(self)}"
+                f"setting nonblocking failed: {self.get_error_message()}"
             )
 
     def flush(self) -> int:
@@ -587,7 +617,7 @@ class PGconn:
             raise e.OperationalError("flushing failed: the connection is closed")
         rv: int = impl.PQflush(self._pgconn_ptr)
         if rv < 0:
-            raise e.OperationalError(f"flushing failed: {error_message(self)}")
+            raise e.OperationalError(f"flushing failed: {self.get_error_message()}")
         return rv
 
     def set_single_row_mode(self) -> None:
@@ -635,13 +665,17 @@ class PGconn:
             buffer = bytes(buffer)
         rv = impl.PQputCopyData(self._pgconn_ptr, buffer, len(buffer))
         if rv < 0:
-            raise e.OperationalError(f"sending copy data failed: {error_message(self)}")
+            raise e.OperationalError(
+                f"sending copy data failed: {self.get_error_message()}"
+            )
         return rv
 
     def put_copy_end(self, error: bytes | None = None) -> int:
         rv = impl.PQputCopyEnd(self._pgconn_ptr, error)
         if rv < 0:
-            raise e.OperationalError(f"sending copy end failed: {error_message(self)}")
+            raise e.OperationalError(
+                f"sending copy end failed: {self.get_error_message()}"
+            )
         return rv
 
     def get_copy_data(self, async_: int) -> tuple[int, memoryview]:
@@ -649,7 +683,7 @@ class PGconn:
         nbytes = impl.PQgetCopyData(self._pgconn_ptr, byref(buffer_ptr), async_)
         if nbytes == -2:
             raise e.OperationalError(
-                f"receiving copy data failed: {error_message(self)}"
+                f"receiving copy data failed: {self.get_error_message()}"
             )
         if buffer_ptr:
             # TODO: do it without copy
@@ -699,7 +733,7 @@ class PGconn:
         out = impl.PQencryptPasswordConn(self._pgconn_ptr, passwd, user, algorithm)
         if not out:
             raise e.OperationalError(
-                f"password encryption failed: {error_message(self)}"
+                f"password encryption failed: {self.get_error_message()}"
             )
 
         rv = string_at(out)
@@ -717,7 +751,7 @@ class PGconn:
         res = impl.PQchangePassword(self._pgconn_ptr, user, passwd)
         if impl.PQresultStatus(res) != ExecStatus.COMMAND_OK:
             raise e.OperationalError(
-                f"failed to change password change command: {error_message(self)}"
+                f"failed to change password change command: {self.get_error_message()}"
             )
 
     def make_empty_result(self, exec_status: int) -> PGresult:
@@ -748,7 +782,7 @@ class PGconn:
             mode.
         """
         if impl.PQexitPipelineMode(self._pgconn_ptr) != 1:
-            raise e.OperationalError(error_message(self))
+            raise e.OperationalError(self.get_error_message())
 
     def pipeline_sync(self) -> None:
         """Mark a synchronization point in a pipeline.
@@ -768,7 +802,9 @@ class PGconn:
         :raises ~e.OperationalError: if the flush request failed.
         """
         if impl.PQsendFlushRequest(self._pgconn_ptr) == 0:
-            raise e.OperationalError(f"flush request failed: {error_message(self)}")
+            raise e.OperationalError(
+                f"flush request failed: {self.get_error_message()}"
+            )
 
     def _call_bytes(self, func: Callable[[impl.PGconn_struct], bytes | None]) -> bytes:
         """
@@ -847,6 +883,9 @@ class PGresult:
     def error_message(self) -> bytes:
         return impl.PQresultErrorMessage(self._pgresult_ptr)
 
+    def get_error_message(self, encoding: str = "utf-8") -> str:
+        return _clean_error_message(self.error_message, encoding)
+
     def error_field(self, fieldcode: int) -> bytes | None:
         return impl.PQresultErrorField(self._pgresult_ptr, fieldcode)
 
@@ -946,8 +985,9 @@ class PGcancelConn:
         See :pq:`PQcancelStart` for details.
         """
         if not impl.PQcancelStart(self.pgcancelconn_ptr):
-            msg = self.error_message.decode("utf8", "replace")
-            raise e.OperationalError(f"couldn't start cancellation: {msg}")
+            raise e.OperationalError(
+                f"couldn't start cancellation: {self.get_error_message()}"
+            )
 
     def blocking(self) -> None:
         """Requests that the server abandons processing of the current command
@@ -956,8 +996,9 @@ class PGcancelConn:
         See :pq:`PQcancelBlocking` for details.
         """
         if not impl.PQcancelBlocking(self.pgcancelconn_ptr):
-            msg = self.error_message.decode("utf8", "replace")
-            raise e.OperationalError(f"couldn't start cancellation: {msg}")
+            raise e.OperationalError(
+                f"couldn't start cancellation: {self.get_error_message()}"
+            )
 
     def poll(self) -> int:
         self._ensure_pgcancelconn()
@@ -978,6 +1019,9 @@ class PGcancelConn:
     def error_message(self) -> bytes:
         return impl.PQcancelErrorMessage(self.pgcancelconn_ptr)
 
+    def get_error_message(self, encoding: str = "utf-8") -> str:
+        return _clean_error_message(self.error_message, encoding)
+
     def reset(self) -> None:
         self._ensure_pgcancelconn()
         impl.PQcancelReset(self.pgcancelconn_ptr)
@@ -1115,7 +1159,7 @@ class Escaping:
         out = impl.PQescapeLiteral(self.conn._pgconn_ptr, data, len(data))
         if not out:
             raise e.OperationalError(
-                f"escape_literal failed: {error_message(self.conn)} bytes"
+                f"escape_literal failed: {self.conn.get_error_message()} bytes"
             )
         rv = string_at(out)
         impl.PQfreemem(out)
@@ -1132,7 +1176,7 @@ class Escaping:
         out = impl.PQescapeIdentifier(self.conn._pgconn_ptr, data, len(data))
         if not out:
             raise e.OperationalError(
-                f"escape_identifier failed: {error_message(self.conn)} bytes"
+                f"escape_identifier failed: {self.conn.get_error_message()} bytes"
             )
         rv = string_at(out)
         impl.PQfreemem(out)
@@ -1156,7 +1200,7 @@ class Escaping:
 
             if error:
                 raise e.OperationalError(
-                    f"escape_string failed: {error_message(self.conn)} bytes"
+                    f"escape_string failed: {self.conn.get_error_message()} bytes"
                 )
 
         else:
index 3f587d2c4129ee210b71f0ea49734bbbec4634f1..70d3c5f8b8a17ac55e5173549a43e36601a357c1 100644 (file)
@@ -24,7 +24,6 @@ from cpython.bytearray cimport PyByteArray_GET_SIZE, PyByteArray_AS_STRING
 from psycopg_c.pq cimport _buffer_as_string_and_size, Escaping
 
 from psycopg import errors as e
-from psycopg.pq.misc import error_message
 
 
 @cython.freelist(8)
index 88c5dc5ad0e18ae1c7e799f3e317278927db8e59..a908577c2a3689d06a973e24f4b16a6a5ca3218e 100644 (file)
@@ -9,7 +9,7 @@ from cpython.object cimport PyObject_CallFunctionObjArgs
 from time import monotonic
 
 from psycopg import errors as e
-from psycopg.pq import abc, error_message
+from psycopg.pq import abc
 from psycopg.abc import PipelineCommand, PQGen
 from psycopg._enums import Wait, Ready
 from psycopg._compat import Deque
@@ -45,7 +45,7 @@ def connect(conninfo: str, *, timeout: float = 0.0) -> PQGenConn[abc.PGconn]:
         if conn_status == libpq.CONNECTION_BAD:
             encoding = conninfo_encoding(conninfo)
             raise e.OperationalError(
-                f"connection is bad: {error_message(conn, encoding=encoding)}",
+                f"connection is bad: {conn.get_error_message(encoding)}",
                 pgconn=conn
             )
 
@@ -67,7 +67,7 @@ def connect(conninfo: str, *, timeout: float = 0.0) -> PQGenConn[abc.PGconn]:
         elif poll_status == libpq.PGRES_POLLING_FAILED:
             encoding = conninfo_encoding(conninfo)
             raise e.OperationalError(
-                f"connection failed: {error_message(conn, encoding=encoding)}",
+                f"connection failed: {conn.get_error_message(encoding)}",
                 pgconn=e.finish_pgconn(conn),
             )
         else:
@@ -100,8 +100,9 @@ def cancel(pq.PGcancelConn cancel_conn, *, timeout: float = 0.0) -> PQGenConn[No
         elif status == libpq.PGRES_POLLING_WRITING:
             yield libpq.PQcancelSocket(pgcancelconn_ptr), WAIT_W
         elif status == libpq.PGRES_POLLING_FAILED:
-            msg = cancel_conn.error_message.decode("utf8", "replace")
-            raise e.OperationalError(f"cancellation failed: {msg}")
+            raise e.OperationalError(
+                f"cancellation failed: {cancel_conn.get_error_message()}"
+            )
         else:
             raise e.InternalError(f"unexpected poll status: {status}")
 
@@ -153,7 +154,7 @@ def send(pq.PGconn pgconn) -> PQGen[None]:
                 cires = libpq.PQconsumeInput(pgconn_ptr)
             if 1 != cires:
                 raise e.OperationalError(
-                    f"consuming input failed: {error_message(pgconn)}")
+                    f"consuming input failed: {pgconn.get_error_message()}")
 
 
 def fetch_many(pq.PGconn pgconn) -> PQGen[list[PGresult]]:
@@ -226,7 +227,7 @@ def fetch(pq.PGconn pgconn) -> PQGen[PGresult | None]:
 
             if 1 != cires:
                 raise e.OperationalError(
-                    f"consuming input failed: {error_message(pgconn)}")
+                    f"consuming input failed: {pgconn.get_error_message()}")
             if not ibres:
                 break
             while True:
@@ -271,7 +272,7 @@ def pipeline_communicate(
                 cires = libpq.PQconsumeInput(pgconn_ptr)
             if 1 != cires:
                 raise e.OperationalError(
-                    f"consuming input failed: {error_message(pgconn)}")
+                    f"consuming input failed: {pgconn.get_error_message()}")
 
             _consume_notifies(pgconn)
 
index d397c17901e7e1b51abe6078a3e9a7f5c485dcb9..26f7779f804a2f8eac869e99b36b037dd161b2e8 100644 (file)
@@ -10,7 +10,6 @@ import logging
 
 from psycopg import errors as e
 from psycopg.pq import Format
-from psycopg.pq.misc import error_message
 
 logger = logging.getLogger("psycopg")
 
index f0a44d37c8937f8f6aec3a7935d411ce452401a4..85ae955a0c0fe7d4a3a4528b4210d0d3aa7fa6a1 100644 (file)
@@ -27,7 +27,7 @@ cdef class Escaping:
         out = libpq.PQescapeLiteral(self.conn._pgconn_ptr, ptr, length)
         if out is NULL:
             raise e.OperationalError(
-                f"escape_literal failed: {error_message(self.conn)}"
+                f"escape_literal failed: {self.conn.get_error_message())}"
             )
 
         rv = out[:strlen(out)]
@@ -49,7 +49,7 @@ cdef class Escaping:
         out = libpq.PQescapeIdentifier(self.conn._pgconn_ptr, ptr, length)
         if out is NULL:
             raise e.OperationalError(
-                f"escape_identifier failed: {error_message(self.conn)}"
+                f"escape_identifier failed: {self.conn.get_error_message()}"
             )
 
         rv = out[:strlen(out)]
@@ -76,7 +76,7 @@ cdef class Escaping:
             if error:
                 PyMem_Free(buf_out)
                 raise e.OperationalError(
-                    f"escape_string failed: {error_message(self.conn)}"
+                    f"escape_string failed: {self.conn.get_error_message()}"
                 )
 
         else:
index 4c461b13e49fb2690f70cdef46125224d3a77b90..8918d1537a94c391129912f74863ceb83a4975d2 100644 (file)
@@ -26,7 +26,7 @@ cdef class PGcancelConn:
         """
         if not libpq.PQcancelStart(self.pgcancelconn_ptr):
             raise e.OperationalError(
-                f"couldn't send cancellation: {self.error_message}"
+                f"couldn't send cancellation: {self.get_error_message()}"
             )
 
     def blocking(self) -> None:
@@ -37,7 +37,7 @@ cdef class PGcancelConn:
         """
         if not libpq.PQcancelBlocking(self.pgcancelconn_ptr):
             raise e.OperationalError(
-                f"couldn't send cancellation: {self.error_message}"
+                f"couldn't send cancellation: {self.get_error_message()}"
             )
 
     def poll(self) -> int:
@@ -59,6 +59,9 @@ cdef class PGcancelConn:
     def error_message(self) -> bytes:
         return libpq.PQcancelErrorMessage(self.pgcancelconn_ptr)
 
+    def get_error_message(self, encoding: str = "utf-8") -> str:
+        return _clean_error_message(self.error_message, encoding)
+
     def reset(self) -> None:
         self._ensure_pgcancelconn()
         libpq.PQcancelReset(self.pgcancelconn_ptr)
index c64929583ad93a5ede11d2280d961e098acb7f89..f6a446f4133a4bcb479a0eddd25e001806dc3f91 100644 (file)
@@ -24,8 +24,9 @@ from cpython.memoryview cimport PyMemoryView_FromObject
 
 import sys
 
+from psycopg._encodings import pg2pyenc
 from psycopg.pq import Format as PqFormat, Trace, version_pretty
-from psycopg.pq.misc import PGnotify, connection_summary
+from psycopg.pq.misc import PGnotify, connection_summary, _clean_error_message
 from psycopg.pq._enums import ExecStatus
 from psycopg_c.pq cimport PQBuffer
 
@@ -174,6 +175,20 @@ cdef class PGconn:
     def error_message(self) -> bytes:
         return libpq.PQerrorMessage(self._pgconn_ptr)
 
+    def get_error_message(self, encoding: str = "") -> str:
+        return _clean_error_message(self.error_message, encoding or self._encoding)
+
+    @property
+    def _encoding(self) -> str:
+        cdef const char *pgenc
+        if libpq.PQstatus(self._pgconn_ptr) == libpq.CONNECTION_OK:
+            pgenc = libpq.PQparameterStatus(self._pgconn_ptr, b"client_encoding")
+            if pgenc is NULL:
+                pgenc = b"UTF8"
+            return pg2pyenc(pgenc)
+        else:
+            return "utf-8"
+
     @property
     def protocol_version(self) -> int:
         return _call_int(self, libpq.PQprotocolVersion)
@@ -211,7 +226,7 @@ cdef class PGconn:
         with nogil:
             pgresult = libpq.PQexec(self._pgconn_ptr, command)
         if pgresult is NULL:
-            raise e.OperationalError(f"executing query failed: {error_message(self)}")
+            raise e.OperationalError(f"executing query failed: {self.get_error_message()}")
 
         return PGresult._from_ptr(pgresult)
 
@@ -221,7 +236,7 @@ cdef class PGconn:
         with nogil:
             rv = libpq.PQsendQuery(self._pgconn_ptr, command)
         if not rv:
-            raise e.OperationalError(f"sending query failed: {error_message(self)}")
+            raise e.OperationalError(f"sending query failed: {self.get_error_message()}")
 
     def exec_params(
         self,
@@ -248,7 +263,7 @@ cdef class PGconn:
                 <const char *const *>cvalues, clengths, cformats, result_format)
         _clear_query_params(ctypes, cvalues, clengths, cformats)
         if pgresult is NULL:
-            raise e.OperationalError(f"executing query failed: {error_message(self)}")
+            raise e.OperationalError(f"executing query failed: {self.get_error_message()}")
         return PGresult._from_ptr(pgresult)
 
     def send_query_params(
@@ -277,7 +292,7 @@ cdef class PGconn:
         _clear_query_params(ctypes, cvalues, clengths, cformats)
         if not rv:
             raise e.OperationalError(
-                f"sending query and params failed: {error_message(self)}"
+                f"sending query and params failed: {self.get_error_message()}"
             )
 
     def send_prepare(
@@ -304,7 +319,7 @@ cdef class PGconn:
         PyMem_Free(atypes)
         if not rv:
             raise e.OperationalError(
-                f"sending query and params failed: {error_message(self)}"
+                f"sending query and params failed: {self.get_error_message()}"
             )
 
     def send_query_prepared(
@@ -332,7 +347,7 @@ cdef class PGconn:
         _clear_query_params(ctypes, cvalues, clengths, cformats)
         if not rv:
             raise e.OperationalError(
-                f"sending prepared query failed: {error_message(self)}"
+                f"sending prepared query failed: {self.get_error_message()}"
             )
 
     def prepare(
@@ -357,7 +372,7 @@ cdef class PGconn:
                 self._pgconn_ptr, name, command, <int>nparams, atypes)
         PyMem_Free(atypes)
         if rv is NULL:
-            raise e.OperationalError(f"preparing query failed: {error_message(self)}")
+            raise e.OperationalError(f"preparing query failed: {self.get_error_message()}")
         return PGresult._from_ptr(rv)
 
     def exec_prepared(
@@ -387,7 +402,7 @@ cdef class PGconn:
         _clear_query_params(ctypes, cvalues, clengths, cformats)
         if rv is NULL:
             raise e.OperationalError(
-                f"executing prepared query failed: {error_message(self)}"
+                f"executing prepared query failed: {self.get_error_message()}"
             )
         return PGresult._from_ptr(rv)
 
@@ -396,7 +411,7 @@ cdef class PGconn:
         cdef libpq.PGresult *rv = libpq.PQdescribePrepared(self._pgconn_ptr, name)
         if rv is NULL:
             raise e.OperationalError(
-                f"describe prepared failed: {error_message(self)}"
+                f"describe prepared failed: {self.get_error_message()}"
             )
         return PGresult._from_ptr(rv)
 
@@ -405,7 +420,7 @@ cdef class PGconn:
         cdef int rv = libpq.PQsendDescribePrepared(self._pgconn_ptr, name)
         if not rv:
             raise e.OperationalError(
-                f"sending describe prepared failed: {error_message(self)}"
+                f"sending describe prepared failed: {self.get_error_message()}"
             )
 
     def describe_portal(self, const char *name) -> PGresult:
@@ -413,7 +428,7 @@ cdef class PGconn:
         cdef libpq.PGresult *rv = libpq.PQdescribePortal(self._pgconn_ptr, name)
         if rv is NULL:
             raise e.OperationalError(
-                f"describe prepared failed: {error_message(self)}"
+                f"describe prepared failed: {self.get_error_message()}"
             )
         return PGresult._from_ptr(rv)
 
@@ -422,7 +437,7 @@ cdef class PGconn:
         cdef int rv = libpq.PQsendDescribePortal(self._pgconn_ptr, name)
         if not rv:
             raise e.OperationalError(
-                f"sending describe prepared failed: {error_message(self)}"
+                f"sending describe prepared failed: {self.get_error_message()}"
             )
 
     def close_prepared(self, const char *name) -> PGresult:
@@ -431,7 +446,7 @@ cdef class PGconn:
         cdef libpq.PGresult *rv = libpq.PQclosePrepared(self._pgconn_ptr, name)
         if rv is NULL:
             raise e.OperationalError(
-                f"close prepared failed: {error_message(self)}"
+                f"close prepared failed: {self.get_error_message()}"
             )
         return PGresult._from_ptr(rv)
 
@@ -441,7 +456,7 @@ cdef class PGconn:
         cdef int rv = libpq.PQsendClosePrepared(self._pgconn_ptr, name)
         if not rv:
             raise e.OperationalError(
-                f"sending close prepared failed: {error_message(self)}"
+                f"sending close prepared failed: {self.get_error_message()}"
             )
 
     def close_portal(self, const char *name) -> PGresult:
@@ -450,7 +465,7 @@ cdef class PGconn:
         cdef libpq.PGresult *rv = libpq.PQclosePortal(self._pgconn_ptr, name)
         if rv is NULL:
             raise e.OperationalError(
-                f"close prepared failed: {error_message(self)}"
+                f"close prepared failed: {self.get_error_message()}"
             )
         return PGresult._from_ptr(rv)
 
@@ -460,7 +475,7 @@ cdef class PGconn:
         cdef int rv = libpq.PQsendClosePortal(self._pgconn_ptr, name)
         if not rv:
             raise e.OperationalError(
-                f"sending close prepared failed: {error_message(self)}"
+                f"sending close prepared failed: {self.get_error_message()}"
             )
 
     def get_result(self) -> "PGresult" | None:
@@ -471,7 +486,7 @@ cdef class PGconn:
 
     def consume_input(self) -> None:
         if 1 != libpq.PQconsumeInput(self._pgconn_ptr):
-            raise e.OperationalError(f"consuming input failed: {error_message(self)}")
+            raise e.OperationalError(f"consuming input failed: {self.get_error_message()}")
 
     def is_busy(self) -> int:
         cdef int rv
@@ -486,14 +501,14 @@ cdef class PGconn:
     @nonblocking.setter
     def nonblocking(self, int arg) -> None:
         if 0 > libpq.PQsetnonblocking(self._pgconn_ptr, arg):
-            raise e.OperationalError(f"setting nonblocking failed: {error_message(self)}")
+            raise e.OperationalError(f"setting nonblocking failed: {self.get_error_message()}")
 
     cpdef int flush(self) except -1:
         if self._pgconn_ptr == NULL:
             raise e.OperationalError(f"flushing failed: the connection is closed")
         cdef int rv = libpq.PQflush(self._pgconn_ptr)
         if rv < 0:
-            raise e.OperationalError(f"flushing failed: {error_message(self)}")
+            raise e.OperationalError(f"flushing failed: {self.get_error_message()}")
         return rv
 
     def set_single_row_mode(self) -> None:
@@ -536,7 +551,7 @@ cdef class PGconn:
         _buffer_as_string_and_size(buffer, &cbuffer, &length)
         rv = libpq.PQputCopyData(self._pgconn_ptr, cbuffer, <int>length)
         if rv < 0:
-            raise e.OperationalError(f"sending copy data failed: {error_message(self)}")
+            raise e.OperationalError(f"sending copy data failed: {self.get_error_message()}")
         return rv
 
     def put_copy_end(self, error: bytes | None = None) -> int:
@@ -546,7 +561,7 @@ cdef class PGconn:
             cerr = PyBytes_AsString(error)
         rv = libpq.PQputCopyEnd(self._pgconn_ptr, cerr)
         if rv < 0:
-            raise e.OperationalError(f"sending copy end failed: {error_message(self)}")
+            raise e.OperationalError(f"sending copy end failed: {self.get_error_message()}")
         return rv
 
     def get_copy_data(self, int async_) -> tuple[int, memoryview]:
@@ -554,7 +569,7 @@ cdef class PGconn:
         cdef int nbytes
         nbytes = libpq.PQgetCopyData(self._pgconn_ptr, &buffer_ptr, async_)
         if nbytes == -2:
-            raise e.OperationalError(f"receiving copy data failed: {error_message(self)}")
+            raise e.OperationalError(f"receiving copy data failed: {self.get_error_message()}")
         if buffer_ptr is not NULL:
             data = PyMemoryView_FromObject(
                 PQBuffer._from_buffer(<unsigned char *>buffer_ptr, nbytes))
@@ -587,7 +602,7 @@ cdef class PGconn:
         out = libpq.PQencryptPasswordConn(self._pgconn_ptr, passwd, user, calgo)
         if not out:
             raise e.OperationalError(
-                f"password encryption failed: {error_message(self)}"
+                f"password encryption failed: {self.get_error_message()}"
             )
 
         rv = bytes(out)
@@ -603,7 +618,7 @@ cdef class PGconn:
         res = libpq.PQchangePassword(self._pgconn_ptr, user, passwd)
         if libpq.PQresultStatus(res) != ExecStatus.COMMAND_OK:
             raise e.OperationalError(
-                f"password encryption failed: {error_message(self)}"
+                f"password encryption failed: {self.get_error_message()}"
             )
 
     def make_empty_result(self, int exec_status) -> PGresult:
@@ -642,7 +657,7 @@ cdef class PGconn:
         """
         _check_supported("PQexitPipelineMode", 140000)
         if libpq.PQexitPipelineMode(self._pgconn_ptr) != 1:
-            raise e.OperationalError(error_message(self))
+            raise e.OperationalError(self.get_error_message())
 
     def pipeline_sync(self) -> None:
         """Mark a synchronization point in a pipeline.
@@ -665,7 +680,7 @@ cdef class PGconn:
         _check_supported("PQsendFlushRequest ", 140000)
         cdef int rv = libpq.PQsendFlushRequest(self._pgconn_ptr)
         if rv == 0:
-            raise e.OperationalError(f"flush request failed: {error_message(self)}")
+            raise e.OperationalError(f"flush request failed: {self.get_error_message()}")
 
 
 cdef int _ensure_pgconn(PGconn pgconn) except 0:
index 2556896d9d340a4b80401a52c6719745852d8a3c..f3fd429926f8189942103d6e8c67fb9c6c2d93e2 100644 (file)
@@ -50,6 +50,9 @@ cdef class PGresult:
     def error_message(self) -> bytes:
         return libpq.PQresultErrorMessage(self._pgresult_ptr)
 
+    def get_error_message(self, encoding: str = "utf-8") -> str:
+        return _clean_error_message(self.error_message, encoding)
+
     def error_field(self, int fieldcode) -> bytes | None:
         cdef char * rv = libpq.PQresultErrorField(self._pgresult_ptr, fieldcode)
         if rv is not NULL:
index cfa0ae143e6034576da16db627571c8d8c45340c..bb1109c05484121500310f7703f6da63b4153f70 100644 (file)
@@ -194,7 +194,7 @@ def pgconn(dsn, request, tracefile):
 
     conn = pq.PGconn.connect(dsn.encode())
     if conn.status != pq.ConnStatus.OK:
-        pytest.fail(f"bad connection: {conn.error_message.decode('utf8', 'replace')}")
+        pytest.fail(f"bad connection: {conn.get_error_message()}")
 
     with maybe_trace(conn, tracefile, request.function):
         yield conn
index 868abce6e75fb13d97417d77407de741efae9f06..3730e14182baa9b88d3eb17c233ed62793efbaea 100644 (file)
@@ -12,12 +12,8 @@ def test_error_message(pgconn):
     assert msg == pq.error_message(res)
     primary = res.error_field(pq.DiagnosticField.MESSAGE_PRIMARY)
     assert primary.decode("ascii") in msg
-
-    with pytest.raises(TypeError):
-        pq.error_message(None)  # type: ignore[arg-type]
-
     res.clear()
-    assert pq.error_message(res) == "no details available"
+    assert pq.error_message(res) == "no error details available"
     pgconn.finish()
     assert "NULL" in pq.error_message(pgconn)
 
index 43db54b00658b4182d758798c1df757b0da49d82..dd82f8d2b82461d7bb5cc28a7bb2b787a5f3ec63 100644 (file)
@@ -330,6 +330,16 @@ def test_error_message(pgconn):
     assert b"NULL" in pgconn.error_message  # TODO: i10n?
 
 
+def test_get_error_message(pgconn):
+    assert pgconn.get_error_message() == "no error details available"
+    res = pgconn.exec_(b"wat")
+    assert res.status == pq.ExecStatus.FATAL_ERROR
+    msg = pgconn.get_error_message()
+    assert "wat" in msg
+    pgconn.finish()
+    assert "NULL" in pgconn.get_error_message()
+
+
 def test_backend_pid(pgconn):
     assert isinstance(pgconn.backend_pid, int)
     pgconn.finish()
@@ -402,7 +412,7 @@ def cancellable_query(pgconn: PGconn) -> Iterator[None]:
     monitor_conn = pq.PGconn.connect(dsn)
     assert (
         monitor_conn.status == pq.ConnStatus.OK
-    ), f"bad connection: {monitor_conn.error_message.decode('utf8', 'replace')}"
+    ), f"bad connection: {monitor_conn.get_error_message()}"
 
     pgconn.send_query_params(b"SELECT pg_sleep($1)", [b"180"])
 
@@ -490,7 +500,7 @@ def test_cancel_conn_finished(pgconn):
         cancel_conn.poll()
     with pytest.raises(psycopg.OperationalError):
         cancel_conn.reset()
-    assert cancel_conn.error_message.strip() == "connection pointer is NULL"
+    assert cancel_conn.get_error_message() == "connection pointer is NULL"
 
 
 def test_cancel(pgconn):
@@ -639,11 +649,11 @@ def role(pgconn: PGconn) -> Iterator[tuple[bytes, bytes]]:
     user, passwd = "ashesh", "psycopg2"
     r = pgconn.exec_(f"CREATE USER {user} LOGIN PASSWORD '{passwd}'".encode())
     if r.status != pq.ExecStatus.COMMAND_OK:
-        pytest.skip(f"cannot create a PostgreSQL role: {r.error_message.decode()}")
+        pytest.skip(f"cannot create a PostgreSQL role: {r.get_error_message()}")
     yield user.encode(), passwd.encode()
     r = pgconn.exec_(f"DROP USER {user}".encode())
     if r.status != pq.ExecStatus.COMMAND_OK:
-        pytest.fail(f"failed to drop {user} role: {r.error_message.decode()}")
+        pytest.fail(f"failed to drop {user} role: {r.get_error_message()}")
 
 
 @pytest.mark.libpq(">= 17")
@@ -686,7 +696,7 @@ def test_encrypt_password_badalgo(pgconn):
 @pytest.mark.crdb_skip("password_encryption")
 def test_encrypt_password_query(pgconn):
     res = pgconn.exec_(b"set password_encryption to 'md5'")
-    assert res.status == pq.ExecStatus.COMMAND_OK, pgconn.error_message.decode()
+    assert res.status == pq.ExecStatus.COMMAND_OK, pgconn.get_error_message()
     enc = pgconn.encrypt_password(b"psycopg2", b"ashesh")
     assert enc == b"md594839d658c28a357126f105b9cb14cfc"
 
index 3ad818d099206257288e21be3a00ec2ec471a1a0..f7072f74e5e356899c923a94961e95bc68bbbb2e 100644 (file)
@@ -50,6 +50,15 @@ def test_error_message(pgconn):
     assert res.error_message == b""
 
 
+def test_get_error_message(pgconn):
+    res = pgconn.exec_(b"select 1")
+    assert res.get_error_message() == "no error details available"
+    res = pgconn.exec_(b"select wat")
+    assert "wat" in res.get_error_message()
+    res.clear()
+    assert res.get_error_message() == "no error details available"
+
+
 def test_error_field(pgconn):
     res = pgconn.exec_(b"select wat")
     # https://github.com/cockroachdb/cockroach/issues/81794