From cec696609b334aa32f871bd342e39b431fc799db Mon Sep 17 00:00:00 2001 From: Daniele Varrazzo Date: Wed, 12 Jun 2024 12:38:04 +0200 Subject: [PATCH] feat: add get_error_message() methods 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. --- psycopg/psycopg/_connection_base.py | 11 +- psycopg/psycopg/_connection_info.py | 3 +- psycopg/psycopg/_copy.py | 3 +- psycopg/psycopg/_copy_async.py | 3 +- psycopg/psycopg/_copy_base.py | 3 +- psycopg/psycopg/_cursor_base.py | 3 +- psycopg/psycopg/_encodings.py | 19 +--- psycopg/psycopg/_pipeline.py | 3 +- psycopg/psycopg/connection.py | 3 +- psycopg/psycopg/connection_async.py | 3 +- psycopg/psycopg/errors.py | 14 ++- psycopg/psycopg/generators.py | 17 ++- psycopg/psycopg/pq/abc.py | 9 ++ psycopg/psycopg/pq/misc.py | 48 ++++----- psycopg/psycopg/pq/pq_ctypes.py | 114 ++++++++++++++------ psycopg_c/psycopg_c/_psycopg/adapt.pyx | 1 - psycopg_c/psycopg_c/_psycopg/generators.pyx | 17 +-- psycopg_c/psycopg_c/pq.pyx | 1 - psycopg_c/psycopg_c/pq/escaping.pyx | 6 +- psycopg_c/psycopg_c/pq/pgcancel.pyx | 7 +- psycopg_c/psycopg_c/pq/pgconn.pyx | 69 +++++++----- psycopg_c/psycopg_c/pq/pgresult.pyx | 3 + tests/fix_db.py | 2 +- tests/pq/test_misc.py | 6 +- tests/pq/test_pgconn.py | 20 +++- tests/pq/test_pgresult.py | 9 ++ 26 files changed, 224 insertions(+), 173 deletions(-) diff --git a/psycopg/psycopg/_connection_base.py b/psycopg/psycopg/_connection_base.py index 8a9a6e50d..091dc6826 100644 --- a/psycopg/psycopg/_connection_base.py +++ b/psycopg/psycopg/_connection_base.py @@ -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}" diff --git a/psycopg/psycopg/_connection_info.py b/psycopg/psycopg/_connection_info.py index 79a781cb0..0db591310 100644 --- a/psycopg/psycopg/_connection_info.py +++ b/psycopg/psycopg/_connection_info.py @@ -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) diff --git a/psycopg/psycopg/_copy.py b/psycopg/psycopg/_copy.py index 1c393a9d6..6d85066d8 100644 --- a/psycopg/psycopg/_copy.py +++ b/psycopg/psycopg/_copy.py @@ -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 diff --git a/psycopg/psycopg/_copy_async.py b/psycopg/psycopg/_copy_async.py index 2c630da9b..02d27bae3 100644 --- a/psycopg/psycopg/_copy_async.py +++ b/psycopg/psycopg/_copy_async.py @@ -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 diff --git a/psycopg/psycopg/_copy_base.py b/psycopg/psycopg/_copy_base.py index d3a143c5f..9217a90e1 100644 --- a/psycopg/psycopg/_copy_base.py +++ b/psycopg/psycopg/_copy_base.py @@ -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 diff --git a/psycopg/psycopg/_cursor_base.py b/psycopg/psycopg/_cursor_base.py index 1f2234513..9448ec505 100644 --- a/psycopg/psycopg/_cursor_base.py +++ b/psycopg/psycopg/_cursor_base.py @@ -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 diff --git a/psycopg/psycopg/_encodings.py b/psycopg/psycopg/_encodings.py index b515f96fd..42da2c529 100644 --- a/psycopg/psycopg/_encodings.py +++ b/psycopg/psycopg/_encodings.py @@ -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: diff --git a/psycopg/psycopg/_pipeline.py b/psycopg/psycopg/_pipeline.py index 441b2147c..6ad3b303f 100644 --- a/psycopg/psycopg/_pipeline.py +++ b/psycopg/psycopg/_pipeline.py @@ -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: diff --git a/psycopg/psycopg/connection.py b/psycopg/psycopg/connection.py index 277d3ab7c..8051a1b5c 100644 --- a/psycopg/psycopg/connection.py +++ b/psycopg/psycopg/connection.py @@ -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) diff --git a/psycopg/psycopg/connection_async.py b/psycopg/psycopg/connection_async.py index d66f6fbf3..8252594b5 100644 --- a/psycopg/psycopg/connection_async.py +++ b/psycopg/psycopg/connection_async.py @@ -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) diff --git a/psycopg/psycopg/errors.py b/psycopg/psycopg/errors.py index 00cb24c9c..ed6c23814 100644 --- a/psycopg/psycopg/errors.py +++ b/psycopg/psycopg/errors.py @@ -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]: diff --git a/psycopg/psycopg/generators.py b/psycopg/psycopg/generators.py index caafde0da..682d7ce4e 100644 --- a/psycopg/psycopg/generators.py +++ b/psycopg/psycopg/generators.py @@ -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 diff --git a/psycopg/psycopg/pq/abc.py b/psycopg/psycopg/pq/abc.py index ed0acb408..e7d617f38 100644 --- a/psycopg/psycopg/pq/abc.py +++ b/psycopg/psycopg/pq/abc.py @@ -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: ... diff --git a/psycopg/psycopg/pq/misc.py b/psycopg/psycopg/pq/misc.py index 6a5294ba0..d5ab483e9 100644 --- a/psycopg/psycopg/pq/misc.py +++ b/psycopg/psycopg/pq/misc.py @@ -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. diff --git a/psycopg/psycopg/pq/pq_ctypes.py b/psycopg/psycopg/pq/pq_ctypes.py index da4f026a7..8335f87f7 100644 --- a/psycopg/psycopg/pq/pq_ctypes.py +++ b/psycopg/psycopg/pq/pq_ctypes.py @@ -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: diff --git a/psycopg_c/psycopg_c/_psycopg/adapt.pyx b/psycopg_c/psycopg_c/_psycopg/adapt.pyx index 3f587d2c4..70d3c5f8b 100644 --- a/psycopg_c/psycopg_c/_psycopg/adapt.pyx +++ b/psycopg_c/psycopg_c/_psycopg/adapt.pyx @@ -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) diff --git a/psycopg_c/psycopg_c/_psycopg/generators.pyx b/psycopg_c/psycopg_c/_psycopg/generators.pyx index 88c5dc5ad..a908577c2 100644 --- a/psycopg_c/psycopg_c/_psycopg/generators.pyx +++ b/psycopg_c/psycopg_c/_psycopg/generators.pyx @@ -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) diff --git a/psycopg_c/psycopg_c/pq.pyx b/psycopg_c/psycopg_c/pq.pyx index d397c1790..26f7779f8 100644 --- a/psycopg_c/psycopg_c/pq.pyx +++ b/psycopg_c/psycopg_c/pq.pyx @@ -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") diff --git a/psycopg_c/psycopg_c/pq/escaping.pyx b/psycopg_c/psycopg_c/pq/escaping.pyx index f0a44d37c..85ae955a0 100644 --- a/psycopg_c/psycopg_c/pq/escaping.pyx +++ b/psycopg_c/psycopg_c/pq/escaping.pyx @@ -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: diff --git a/psycopg_c/psycopg_c/pq/pgcancel.pyx b/psycopg_c/psycopg_c/pq/pgcancel.pyx index 4c461b13e..8918d1537 100644 --- a/psycopg_c/psycopg_c/pq/pgcancel.pyx +++ b/psycopg_c/psycopg_c/pq/pgcancel.pyx @@ -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) diff --git a/psycopg_c/psycopg_c/pq/pgconn.pyx b/psycopg_c/psycopg_c/pq/pgconn.pyx index c64929583..f6a446f41 100644 --- a/psycopg_c/psycopg_c/pq/pgconn.pyx +++ b/psycopg_c/psycopg_c/pq/pgconn.pyx @@ -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: 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, 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, 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(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: diff --git a/psycopg_c/psycopg_c/pq/pgresult.pyx b/psycopg_c/psycopg_c/pq/pgresult.pyx index 2556896d9..f3fd42992 100644 --- a/psycopg_c/psycopg_c/pq/pgresult.pyx +++ b/psycopg_c/psycopg_c/pq/pgresult.pyx @@ -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: diff --git a/tests/fix_db.py b/tests/fix_db.py index cfa0ae143..bb1109c05 100644 --- a/tests/fix_db.py +++ b/tests/fix_db.py @@ -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 diff --git a/tests/pq/test_misc.py b/tests/pq/test_misc.py index 868abce6e..3730e1418 100644 --- a/tests/pq/test_misc.py +++ b/tests/pq/test_misc.py @@ -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) diff --git a/tests/pq/test_pgconn.py b/tests/pq/test_pgconn.py index 43db54b00..dd82f8d2b 100644 --- a/tests/pq/test_pgconn.py +++ b/tests/pq/test_pgconn.py @@ -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" diff --git a/tests/pq/test_pgresult.py b/tests/pq/test_pgresult.py index 3ad818d09..f7072f74e 100644 --- a/tests/pq/test_pgresult.py +++ b/tests/pq/test_pgresult.py @@ -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 -- 2.47.3