From: Daniele Varrazzo Date: Sun, 27 Dec 2020 16:12:51 +0000 (+0100) Subject: Added repr of several objects X-Git-Tag: 3.0.dev0~242 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=b4333bd8a841a3d940da2bf9847595d79d61ee7b;p=thirdparty%2Fpsycopg.git Added repr of several objects Show the underlying connection basic identification and status. --- diff --git a/psycopg3/psycopg3/connection.py b/psycopg3/psycopg3/connection.py index a5c9aa529..059212332 100644 --- a/psycopg3/psycopg3/connection.py +++ b/psycopg3/psycopg3/connection.py @@ -27,7 +27,7 @@ from . import cursor from . import errors as e from . import waiting from . import encodings -from .pq import TransactionStatus, ExecStatus, Format +from .pq import ConnStatus, ExecStatus, TransactionStatus, Format from .sql import Composable from .proto import PQGen, PQGenConn, RV, Query, Params, AdaptContext from .proto import ConnectionType @@ -128,21 +128,22 @@ class BaseConnection(AdaptContext): if status == TransactionStatus.UNKNOWN: return - elif status == TransactionStatus.INTRANS: - msg = ( - f"connection {self} was deleted with an open transaction," - " changes discarded by the server" - ) - else: - status = TransactionStatus(status) # in case we got an int - msg = f"connection {self} was deleted open in status {status.name}" + status = TransactionStatus(status) # in case we got an int + warnings.warn( + f"connection {self} was deleted while still open." + f" Please use 'with' or '.close()' to close the connection", + ResourceWarning, + ) - warnings.warn(msg, ResourceWarning) + def __repr__(self) -> str: + cls = f"{self.__class__.__module__}.{self.__class__.__qualname__}" + info = pq.misc.connection_summary(self.pgconn) + return f"<{cls} {info} at 0x{id(self):x}>" @property def closed(self) -> bool: """`True` if the connection is closed.""" - return self.pgconn.status == self.ConnStatus.BAD + return self.pgconn.status == ConnStatus.BAD @property def autocommit(self) -> bool: @@ -325,8 +326,8 @@ class BaseConnection(AdaptContext): Only used to implement internal commands such as commit, returning no result. The cursor can do more complex stuff. """ - if self.pgconn.status != self.ConnStatus.OK: - if self.pgconn.status == self.ConnStatus.BAD: + if self.pgconn.status != ConnStatus.OK: + if self.pgconn.status == ConnStatus.BAD: raise e.OperationalError("the connection is closed") raise e.InterfaceError( f"cannot execute operations: the connection is" diff --git a/psycopg3/psycopg3/copy.py b/psycopg3/psycopg3/copy.py index 2fcbe7ca4..76cd25f8c 100644 --- a/psycopg3/psycopg3/copy.py +++ b/psycopg3/psycopg3/copy.py @@ -10,6 +10,7 @@ from typing import TYPE_CHECKING, AsyncIterator, Iterator, Generic from typing import Any, Dict, List, Match, Optional, Sequence, Type, Union from types import TracebackType +from . import pq from .pq import Format, ExecStatus from .proto import ConnectionType from .generators import copy_from, copy_to, copy_end @@ -41,6 +42,11 @@ class BaseCopy(Generic[ConnectionType]): else: self._format_copy_row = self._format_row_binary + def __repr__(self) -> str: + cls = f"{self.__class__.__module__}.{self.__class__.__qualname__}" + info = pq.misc.connection_summary(self.connection.pgconn) + return f"<{cls} {info} at 0x{id(self):x}>" + def _format_row(self, row: Sequence[Any]) -> bytes: """Convert a Python sequence to the data to send for copy""" out: List[Optional[bytes]] = [] diff --git a/psycopg3/psycopg3/cursor.py b/psycopg3/psycopg3/cursor.py index 08811862c..100fa513e 100644 --- a/psycopg3/psycopg3/cursor.py +++ b/psycopg3/psycopg3/cursor.py @@ -71,6 +71,12 @@ class BaseCursor(Generic[ConnectionType]): self._query: Optional[bytes] = None self._params: Optional[List[Optional[bytes]]] = None + def __repr__(self) -> str: + cls = f"{self.__class__.__module__}.{self.__class__.__qualname__}" + info = pq.misc.connection_summary(self._conn.pgconn) + status = " (closed)" if self._closed else "" + return f"<{cls}{status} {info} at 0x{id(self):x}>" + @property def connection(self) -> ConnectionType: """The connection this cursor is using.""" diff --git a/psycopg3/psycopg3/pq/misc.py b/psycopg3/psycopg3/pq/misc.py index 450a2d190..7d9cb9763 100644 --- a/psycopg3/psycopg3/pq/misc.py +++ b/psycopg3/psycopg3/pq/misc.py @@ -7,7 +7,7 @@ Various functionalities to make easier to work with the libpq. from typing import cast, NamedTuple, Optional, Union from ..errors import OperationalError -from ._enums import DiagnosticField, ConnStatus +from ._enums import DiagnosticField, ConnStatus, TransactionStatus from .proto import PGconn, PGresult @@ -91,3 +91,29 @@ def error_message(obj: Union[PGconn, PGresult], encoding: str = "utf8") -> str: msg = "no details available" return msg + + +def connection_summary(pgconn: PGconn) -> str: + """ + Return summary information on a connection. + + Useful for __repr__ + """ + parts = [] + if pgconn.status == ConnStatus.OK: + + status = TransactionStatus(pgconn.transaction_status).name + if not pgconn.host.startswith(b"/"): + parts.append(("host", pgconn.host.decode("utf-8"))) + if pgconn.port != b"5432": + parts.append(("port", pgconn.port.decode("utf-8"))) + if pgconn.user != pgconn.db: + parts.append(("user", pgconn.user.decode("utf-8"))) + parts.append(("database", pgconn.db.decode("utf-8"))) + else: + status = ConnStatus(pgconn.status).name + + sparts = " ".join("%s=%s" % part for part in parts) + if sparts: + sparts = f" ({sparts})" + return f"[{status}]{sparts}" diff --git a/psycopg3/psycopg3/pq/pq_ctypes.py b/psycopg3/psycopg3/pq/pq_ctypes.py index d7b51f181..27adb3739 100644 --- a/psycopg3/psycopg3/pq/pq_ctypes.py +++ b/psycopg3/psycopg3/pq/pq_ctypes.py @@ -20,7 +20,7 @@ from typing import cast as t_cast, TYPE_CHECKING from . import _pq_ctypes as impl from .misc import PGnotify, ConninfoOption, PQerror, PGresAttDesc -from .misc import error_message +from .misc import error_message, connection_summary from ._enums import ConnStatus, DiagnosticField, ExecStatus, Format from ._enums import Ping, PollingStatus, TransactionStatus @@ -88,6 +88,11 @@ class PGconn: if os.getpid() == self._procpid: self.finish() + def __repr__(self) -> str: + cls = f"{self.__class__.__module__}.{self.__class__.__qualname__}" + info = connection_summary(self) + return f"<{cls} {info} at 0x{id(self):x}>" + @classmethod def connect(cls, conninfo: bytes) -> "PGconn": if not isinstance(conninfo, bytes): diff --git a/psycopg3/psycopg3/transaction.py b/psycopg3/psycopg3/transaction.py index 6be6e34f4..7b4700639 100644 --- a/psycopg3/psycopg3/transaction.py +++ b/psycopg3/psycopg3/transaction.py @@ -9,6 +9,7 @@ import logging from types import TracebackType from typing import Generic, Optional, Type, Union, TYPE_CHECKING +from . import pq from . import sql from .pq import TransactionStatus from .proto import ConnectionType, PQGen @@ -50,7 +51,7 @@ class BaseTransaction(Generic[ConnectionType]): self._conn = connection self._savepoint_name = savepoint_name or "" self.force_rollback = force_rollback - self._yolo = True + self._entered = self._exited = False @property def connection(self) -> ConnectionType: @@ -67,18 +68,22 @@ class BaseTransaction(Generic[ConnectionType]): return self._savepoint_name def __repr__(self) -> str: - args = [f"connection={self.connection}"] - if not self.savepoint_name: - args.append(f"savepoint_name={self.savepoint_name!r}") - if self.force_rollback: - args.append("force_rollback=True") - return f"{self.__class__.__qualname__}({', '.join(args)})" + cls = f"{self.__class__.__module__}.{self.__class__.__qualname__}" + info = pq.misc.connection_summary(self._conn.pgconn) + if not self._entered: + status = "inactive" + elif not self._exited: + status = "active" + else: + status = "terminated" + + sp = f"{self.savepoint_name!r} " if self.savepoint_name else "" + return f"<{cls} {sp}({status}) {info} at 0x{id(self):x}>" def _enter_gen(self) -> PQGen[None]: - if not self._yolo: + if self._entered: raise TypeError("transaction blocks can be used only once") - else: - self._yolo = False + self._entered = True self._outer_transaction = ( self._conn.pgconn.transaction_status == TransactionStatus.IDLE @@ -124,6 +129,7 @@ class BaseTransaction(Generic[ConnectionType]): def _commit_gen(self) -> PQGen[None]: assert self._conn._savepoints[-1] == self._savepoint_name self._conn._savepoints.pop() + self._exited = True commands = [] if self._savepoint_name and not self._outer_transaction: diff --git a/psycopg3_c/psycopg3_c/pq/pgconn.pyx b/psycopg3_c/psycopg3_c/pq/pgconn.pyx index 0cf4af703..e13ffd551 100644 --- a/psycopg3_c/psycopg3_c/pq/pgconn.pyx +++ b/psycopg3_c/psycopg3_c/pq/pgconn.pyx @@ -11,7 +11,7 @@ from cpython.bytes cimport PyBytes_AsString import logging from psycopg3_c.pq.libpq cimport Oid -from psycopg3.pq.misc import PGnotify +from psycopg3.pq.misc import PGnotify, connection_summary logger = logging.getLogger('psycopg3') @@ -35,6 +35,11 @@ cdef class PGconn: if self._procpid == getpid(): self.finish() + def __repr__(self) -> str: + cls = f"{self.__class__.__module__}.{self.__class__.__qualname__}" + info = connection_summary(self) + return f"<{cls} {info} at 0x{id(self):x}>" + @classmethod def connect(cls, const char *conninfo) -> PGconn: cdef libpq.PGconn* pgconn = libpq.PQconnectdb(conninfo) diff --git a/tests/pq/test_pgconn.py b/tests/pq/test_pgconn.py index 01e2e2362..cc579a60e 100644 --- a/tests/pq/test_pgconn.py +++ b/tests/pq/test_pgconn.py @@ -438,3 +438,13 @@ def test_notice_error(pgconn, caplog): rec = caplog.records[0] assert rec.levelno == logging.ERROR assert "hello error" in rec.message + + +def test_str(pgconn, dsn): + assert "[IDLE]" in str(pgconn) + pgconn.finish() + assert "[BAD]" in str(pgconn) + + pgconn2 = pq.PGconn.connect_start(dsn.encode("utf8")) + assert "[" in str(pgconn2) + assert "[IDLE]" not in str(pgconn2) diff --git a/tests/test_connection.py b/tests/test_connection.py index 62366fbe8..79c725db1 100644 --- a/tests/test_connection.py +++ b/tests/test_connection.py @@ -85,7 +85,7 @@ def test_connection_warn_close(dsn, recwarn): conn = Connection.connect(dsn) conn.execute("select 1") del conn - assert "discarded" in str(recwarn.pop(ResourceWarning).message) + assert "INTRANS" in str(recwarn.pop(ResourceWarning).message) conn = Connection.connect(dsn) try: @@ -467,3 +467,9 @@ def test_execute(conn): cur = conn.execute("select 12, 22") assert cur.fetchone() == (12, 22) + + +def test_str(conn): + assert "[IDLE]" in str(conn) + conn.close() + assert "[BAD]" in str(conn) diff --git a/tests/test_connection_async.py b/tests/test_connection_async.py index 206fda2f6..f24a56907 100644 --- a/tests/test_connection_async.py +++ b/tests/test_connection_async.py @@ -90,7 +90,7 @@ async def test_connection_warn_close(dsn, recwarn): conn = await AsyncConnection.connect(dsn) await conn.execute("select 1") del conn - assert "discarded" in str(recwarn.pop(ResourceWarning).message) + assert "INTRANS" in str(recwarn.pop(ResourceWarning).message) conn = await AsyncConnection.connect(dsn) try: @@ -485,3 +485,9 @@ async def test_execute(aconn): cur = await aconn.execute("select 12, 22") assert await cur.fetchone() == (12, 22) + + +async def test_str(aconn): + assert "[IDLE]" in str(aconn) + await aconn.close() + assert "[BAD]" in str(aconn) diff --git a/tests/test_copy.py b/tests/test_copy.py index 2ad60d243..eb2c17433 100644 --- a/tests/test_copy.py +++ b/tests/test_copy.py @@ -309,6 +309,15 @@ def test_cant_reenter(conn): list(copy) +def test_str(conn): + cur = conn.cursor() + with cur.copy("copy (select 1) to stdout") as copy: + assert "[ACTIVE]" in str(copy) + list(copy) + + assert "[INTRANS]" in str(copy) + + def ensure_table(cur, tabledef, name="copy_in"): cur.execute(f"drop table if exists {name}") cur.execute(f"create table {name} ({tabledef})") diff --git a/tests/test_copy_async.py b/tests/test_copy_async.py index ac25b1d20..57027064b 100644 --- a/tests/test_copy_async.py +++ b/tests/test_copy_async.py @@ -300,6 +300,16 @@ async def test_cant_reenter(aconn): pass +async def test_str(aconn): + cur = await aconn.cursor() + async with cur.copy("copy (select 1) to stdout") as copy: + assert "[ACTIVE]" in str(copy) + async for record in copy: + pass + + assert "[INTRANS]" in str(copy) + + async def ensure_table(cur, tabledef, name="copy_in"): await cur.execute(f"drop table if exists {name}") await cur.execute(f"create table {name} ({tabledef})") diff --git a/tests/test_cursor.py b/tests/test_cursor.py index ba121f9e6..a607833da 100644 --- a/tests/test_cursor.py +++ b/tests/test_cursor.py @@ -368,3 +368,12 @@ class TestColumn: pickled = pickle.dumps(description, pickle.HIGHEST_PROTOCOL) unpickled = pickle.loads(pickled) assert [tuple(d) for d in description] == [tuple(d) for d in unpickled] + + +def test_str(conn): + cur = conn.cursor() + assert "[IDLE]" in str(cur) + assert "(closed)" not in str(cur) + cur.close() + assert "(closed)" in str(cur) + assert "[IDLE]" in str(cur) diff --git a/tests/test_cursor_async.py b/tests/test_cursor_async.py index 68052270f..d04f2ba55 100644 --- a/tests/test_cursor_async.py +++ b/tests/test_cursor_async.py @@ -278,3 +278,12 @@ async def test_iter_stop(aconn): assert (await cur.fetchone()) == (3,) async for rec in cur: assert False + + +async def test_str(aconn): + cur = await aconn.cursor() + assert "[IDLE]" in str(cur) + assert "(closed)" not in str(cur) + await cur.close() + assert "(closed)" in str(cur) + assert "[IDLE]" in str(cur) diff --git a/tests/test_transaction.py b/tests/test_transaction.py index 5458216cd..5f48edb92 100644 --- a/tests/test_transaction.py +++ b/tests/test_transaction.py @@ -625,3 +625,16 @@ def test_explicit_rollback_of_enclosing_tx_outer_tx_unaffected(conn, svcconn): assert not inserted(svcconn) # Not yet committed # Changes committed assert inserted(svcconn) == {"outer-before", "outer-after"} + + +def test_str(conn): + with conn.transaction() as tx: + assert "[INTRANS]" in str(tx) + assert "(active)" in str(tx) + assert "'" not in str(tx) + with conn.transaction("wat") as tx2: + assert "[INTRANS]" in str(tx2) + assert "'wat'" in str(tx2) + + assert "[IDLE]" in str(tx) + assert "(terminated)" in str(tx) diff --git a/tests/test_transaction_async.py b/tests/test_transaction_async.py index f3d1582e9..2bfab9301 100644 --- a/tests/test_transaction_async.py +++ b/tests/test_transaction_async.py @@ -563,3 +563,16 @@ async def test_explicit_rollback_of_enclosing_tx_outer_tx_unaffected( assert not inserted(svcconn) # Not yet committed # Changes committed assert inserted(svcconn) == {"outer-before", "outer-after"} + + +async def test_str(aconn): + async with aconn.transaction() as tx: + assert "[INTRANS]" in str(tx) + assert "(active)" in str(tx) + assert "'" not in str(tx) + async with aconn.transaction("wat") as tx2: + assert "[INTRANS]" in str(tx2) + assert "'wat'" in str(tx2) + + assert "[IDLE]" in str(tx) + assert "(terminated)" in str(tx)