]> git.ipfire.org Git - thirdparty/psycopg.git/commitdiff
Added repr of several objects
authorDaniele Varrazzo <daniele.varrazzo@gmail.com>
Sun, 27 Dec 2020 16:12:51 +0000 (17:12 +0100)
committerDaniele Varrazzo <daniele.varrazzo@gmail.com>
Sun, 27 Dec 2020 16:12:51 +0000 (17:12 +0100)
Show the underlying connection basic identification and status.

16 files changed:
psycopg3/psycopg3/connection.py
psycopg3/psycopg3/copy.py
psycopg3/psycopg3/cursor.py
psycopg3/psycopg3/pq/misc.py
psycopg3/psycopg3/pq/pq_ctypes.py
psycopg3/psycopg3/transaction.py
psycopg3_c/psycopg3_c/pq/pgconn.pyx
tests/pq/test_pgconn.py
tests/test_connection.py
tests/test_connection_async.py
tests/test_copy.py
tests/test_copy_async.py
tests/test_cursor.py
tests/test_cursor_async.py
tests/test_transaction.py
tests/test_transaction_async.py

index a5c9aa52977954ab41c8e97c7bd07b204d456c05..0592123325948a2d135197377c83c5f08496b39c 100644 (file)
@@ -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"
index 2fcbe7ca42940d33526243027799c5b2a48ca5f6..76cd25f8cd8bc1f7639c59f2864aa2498e91a810 100644 (file)
@@ -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]] = []
index 08811862c4cece8b22380f9eecdc22e4ba286c81..100fa513e2a5a99157d89d675bb132e042e103e3 100644 (file)
@@ -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."""
index 450a2d190a0cc56c9866960ab569ae5ae9f899bc..7d9cb97636abaa95e5fa2d1b729e345513d41465 100644 (file)
@@ -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}"
index d7b51f18113201e161c3cace373b24639dc719db..27adb37394fb712f84788cd3da85a7274e8b6140 100644 (file)
@@ -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):
index 6be6e34f48fcdcb110019b4bd28127b64fb74d41..7b4700639e7abbd138a97ee967627440b16cbb9d 100644 (file)
@@ -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:
index 0cf4af703c2c0bb162d51f9fcb805ea51c1ef558..e13ffd55127773d69edf1ee5368c5f423c6f69e3 100644 (file)
@@ -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)
index 01e2e2362ae0a579df1174a06d84a2b3f588e5bd..cc579a60ecf4365c41629e43f9cdb62d8110b5d0 100644 (file)
@@ -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)
index 62366fbe833aad3a2311a559d98977bc8f820210..79c725db1f0e8ff33858039a6c7e1b0f45e6e59d 100644 (file)
@@ -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)
index 206fda2f6fd637f9494e7321700391a42fe62bd3..f24a56907836064ab5717f6e157ed081992862c7 100644 (file)
@@ -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)
index 2ad60d2433197110cae024672e47ba781c6aca3d..eb2c17433fe1f25d09bfb8359ab9462f8b9f1a3d 100644 (file)
@@ -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})")
index ac25b1d20268550bb66e8c063b02be0d5844fcce..57027064b7e988f1cc5d46057914e0eb98982822 100644 (file)
@@ -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})")
index ba121f9e6cdc6e5ee93649b32ebc22bd4a536f5b..a607833dac611b1386dbe012cf8bc145dfa614d1 100644 (file)
@@ -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)
index 68052270f702a36991e45d30f83ad2950e62de59..d04f2ba555384f4b96f333f65ef7749c082df091 100644 (file)
@@ -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)
index 5458216cd57caf59046666784667a4feeb0b7723..5f48edb92788d8f69f233bfc8e6293bc8e1721d4 100644 (file)
@@ -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)
index f3d1582e92628fc26499eb44f6a18d6fe0b75c79..2bfab93018dd0a5401579886db4a3a592c83b764 100644 (file)
@@ -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)