From: Daniele Varrazzo Date: Thu, 12 Nov 2020 15:25:49 +0000 (+0000) Subject: First cut of cursor documentation X-Git-Tag: 3.0.dev0~372 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=c199c43cdf38a748f654f901749cad5fbb20212b;p=thirdparty%2Fpsycopg.git First cut of cursor documentation --- diff --git a/docs/connection.rst b/docs/connection.rst index 5ccb9f7a0..d0317823f 100644 --- a/docs/connection.rst +++ b/docs/connection.rst @@ -1,5 +1,5 @@ -The ``Connection`` classes -========================== +Connection classes +================== .. currentmodule:: psycopg3 @@ -12,12 +12,18 @@ usually handles a transaction automatically: other sessions will not be able to see the changes until you have committed them, more or less explicitly. Take a look to :ref:`transactions` for the details. + +The `!Connection` class +----------------------- + .. autoclass:: Connection - This class implements a DBAPI-compliant interface. It is what you want to - use if you write a "classic", blocking program (eventually using threads or - Eventlet/gevent for concurrency. If your program uses `asyncio` you might - want to use `AsyncConnection` instead. + This class implements a `DBAPI-compliant interface`__. It is what you want + to use if you write a "classic", blocking program (eventually using + threads or Eventlet/gevent for concurrency. If your program uses `asyncio` + you might want to use `AsyncConnection` instead. + + .. __: https://www.python.org/dev/peps/pep-0249/#connection-objects Connections behave as context managers: on block exit, the current transaction will be committed (or rolled back, in case of exception) and @@ -89,13 +95,16 @@ Take a look to :ref:`transactions` for the details. TODO: document `Diagnostic` +The `!AsyncConnection` class +---------------------------- + .. autoclass:: AsyncConnection This class implements a DBAPI-inspired interface, with all the blocking methods implemented as coroutines. Unless specified otherwise, non-blocking methods are shared with the `Connection` class. - The following methods have the same behaviour of the matching `~Connection` + The following methods have the same behaviour of the matching `!Connection` methods, but have an `async` interface. .. automethod:: connect @@ -108,5 +117,8 @@ Take a look to :ref:`transactions` for the details. .. automethod:: set_autocommit +Connection support objects +-------------------------- + .. autoclass:: Notify :members: channel, payload, pid diff --git a/docs/cursor.rst b/docs/cursor.rst new file mode 100644 index 000000000..281bccb2c --- /dev/null +++ b/docs/cursor.rst @@ -0,0 +1,142 @@ +Cursor classes +============== + +.. currentmodule:: psycopg3 + +The `Cursor` and `AsyncCursor` classes are the main objects to send commands +to a PostgreSQL database session. They are normally created by the +`~Connection.cursor()` method. + +A `Connection` can create several cursors, but only one at time can perform +operations, so they are not the best way to achieve parallelism (you may want +to operate with several connections instead). All the cursors on the same +connection have a view of the same session, so they can see each other's +uncommitted data. + + +The `!Cursor` class +------------------- + +.. autoclass:: Cursor + + This class implements `DBAPI-compliant interface`__. It is what the + classic `Connection.cursor()` method returns. `AsyncConnection.cursor()` + will create instead `AsyncCursor` objects, which have the same set of + method but expose an `asyncio` interface and require ``async`` and + ``await`` keywords to operate. + + .. __: https://www.python.org/dev/peps/pep-0249/#cursor-objects + + Cursors behave as context managers: on block exit they are closed and + further operation will not be possible. Closing a cursor will not + terminate a transaction or a session though. + + .. automethod:: close + .. autoproperty:: closed + + .. rubric:: Methods to send commands + + .. automethod:: execute + + Return the cursor itself, so that it will be possible to chain a fetch + operation after the call + + See :ref:`query-parameters` for all the details about executing + queries. + + .. automethod:: executemany + + This is more efficient than performing separate queries, but in case of + several :sql:`INSERT` (and with some SQL creativity for massive + :sql:`UPDATE` too) you may consider using `copy()`. + + See :ref:`query-parameters` for all the details about executing + queries. + + .. automethod:: copy + + See :ref:`copy` for information about :sql:`COPY`. + + .. automethod:: callproc + + This method exists for DBAPI compatibility but it's not much different + than calling `execute()` on a :sql:`SELECT myproc(%s, %s, ...)`, which + will give you more flexibility in passing arguments and retrieving + results. Don't bother... + + .. rubric:: Methods to retrieve results + + Fetch methods are only available if the last operation produced results, + e.g. a :sql:`SELECT` or a command with :sql:`RETURNING`. They will raise + an exception if used with operations that don't return result, such as an + :sql:`INSERT` with no :sql:`RETURNING` or an :sql:`ALTER TABLE`. + + Cursors are iterable objects, so just using ``for var in cursor`` syntax + will iterate on the records in the current recordset. + + .. automethod:: fetchone + .. automethod:: fetchmany + .. automethod:: fetchall + .. automethod:: nextset + .. autoattribute:: pgresult + + .. rubric:: Information about the data + + .. autoproperty:: description + .. autoproperty:: rowcount + + +The `!AsyncCursor` class +------------------------ + +.. autoclass:: AsyncCursor + + This class implements a DBAPI-inspired interface, with all the blocking + methods implemented as coroutines. Unless specified otherwise, + non-blocking methods are shared with the `Cursor` class. + + The following methods have the same behaviour of the matching `!Cursor` + methods, but have an `async` interface. + + .. automethod:: close + .. automethod:: execute + .. automethod:: executemany + .. automethod:: copy + .. automethod:: callproc + .. automethod:: fetchone + .. automethod:: fetchmany + .. automethod:: fetchall + + +Cursor support objects +---------------------- + +.. autoclass:: Column + + An object describing a column of data from a database result, `as described + by the DBAPI`__, so it can also be unpacked as a 7-items tuple + + .. __: https://www.python.org/dev/peps/pep-0249/#description + + .. autoproperty:: name + .. autoproperty:: type_code + .. autoproperty:: display_size + .. autoproperty:: internal_size + .. autoproperty:: precision + .. autoproperty:: scale + + +.. autoclass:: Copy + + .. automethod:: read + .. automethod:: write + .. automethod:: write_row + .. automethod:: finish + + +.. autoclass:: AsyncCopy + + .. automethod:: read + .. automethod:: write + .. automethod:: write_row + .. automethod:: finish diff --git a/docs/index.rst b/docs/index.rst index d0d3a7f25..7f841ecac 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -23,6 +23,7 @@ the COPY support. usage from_pg2 connection + cursor Indices and tables diff --git a/docs/usage.rst b/docs/usage.rst index 1ee333160..4e86a787c 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -134,10 +134,21 @@ TODO: lift from psycopg2 docs Transaction management ---------------------- -TODO: +TODO +.. index:: + pair: COPY; SQL command + +.. _copy: + +Using COPY TO and COPY FROM +--------------------------- + +TODO + + .. index:: async Async operations diff --git a/psycopg3/psycopg3/connection.py b/psycopg3/psycopg3/connection.py index dd1b04f45..7737098e2 100644 --- a/psycopg3/psycopg3/connection.py +++ b/psycopg3/psycopg3/connection.py @@ -84,7 +84,7 @@ class BaseConnection: TransactionStatus = pq.TransactionStatus def __init__(self, pgconn: pq.proto.PGconn): - self.pgconn = pgconn + self.pgconn = pgconn # TODO: document this self.cursor_factory = cursor.BaseCursor self._autocommit = False self.dumpers: proto.DumpersMap = {} @@ -99,7 +99,7 @@ class BaseConnection: @property def closed(self) -> bool: - """`true` if the connection is closed.""" + """`True` if the connection is closed.""" return self.pgconn.status == self.ConnStatus.BAD @property @@ -254,7 +254,9 @@ class Connection(BaseConnection): def cursor( self, name: str = "", format: pq.Format = pq.Format.TEXT ) -> cursor.Cursor: - """Return a new cursor to send commands and query the connection.""" + """ + Return a new `Cursor` to send commands and queries to the connection. + """ cur = self._cursor(name, format=format) return cast(cursor.Cursor, cur) @@ -381,6 +383,9 @@ class AsyncConnection(BaseConnection): async def cursor( self, name: str = "", format: pq.Format = pq.Format.TEXT ) -> cursor.AsyncCursor: + """ + Return a new `AsyncCursor` to send commands and queries to the connection. + """ cur = self._cursor(name, format=format) return cast(cursor.AsyncCursor, cur) diff --git a/psycopg3/psycopg3/cursor.py b/psycopg3/psycopg3/cursor.py index ef7d2bc0e..18c76afd8 100644 --- a/psycopg3/psycopg3/cursor.py +++ b/psycopg3/psycopg3/cursor.py @@ -77,6 +77,7 @@ class Column(Sequence[Any]): @property def name(self) -> str: + """The name of the column.""" rv = self._pgresult.fname(self._index) if rv: return rv.decode(self._encoding) @@ -87,10 +88,12 @@ class Column(Sequence[Any]): @property def type_code(self) -> int: + """The numeric OID of the column.""" return self._pgresult.ftype(self._index) @property def display_size(self) -> Optional[int]: + """The field size, for :sql:`varchar(n)`, None otherwise.""" t = builtins.get(self.type_code) if not t: return None @@ -104,11 +107,13 @@ class Column(Sequence[Any]): @property def internal_size(self) -> Optional[int]: + """The interal field size for fixed-size types, None otherwise.""" fsize = self._pgresult.fsize(self._index) return fsize if fsize >= 0 else None @property def precision(self) -> Optional[int]: + """The number of digits for fixed precision types.""" t = builtins.get(self.type_code) if not t: return None @@ -128,6 +133,10 @@ class Column(Sequence[Any]): @property def scale(self) -> Optional[int]: + """The number of digits after the decimal point if available. + + TODO: probably better than precision for datetime objects? review. + """ if self.type_code == builtins["numeric"].oid: fmod = self._pgresult.fmod(self._index) - 4 if fmod >= 0: @@ -137,6 +146,7 @@ class Column(Sequence[Any]): @property def null_ok(self) -> Optional[bool]: + """Always `None`""" return None @@ -165,15 +175,18 @@ class BaseCursor: @property def closed(self) -> bool: + """`True` if the cursor is closed.""" return self._closed @property def status(self) -> Optional[pq.ExecStatus]: + # TODO: do we want this? res = self.pgresult return res.status if res else None @property def pgresult(self) -> Optional[pq.proto.PGresult]: + """The `~psycopg3.pq.PGresult` exposed by the cursor.""" return self._pgresult @pgresult.setter @@ -184,6 +197,11 @@ class BaseCursor: @property def description(self) -> Optional[List[Column]]: + """ + A list of `Column` object describing the current resultset. + + `None` if the current resulset didn't return tuples. + """ res = self.pgresult if not res or res.status != self.ExecStatus.TUPLES_OK: return None @@ -192,6 +210,7 @@ class BaseCursor: @property def rowcount(self) -> int: + """Number of records affected by the precedent operation.""" return self._rowcount def setinputsizes(self, sizes: Sequence[Any]) -> None: @@ -203,6 +222,9 @@ class BaseCursor: pass def nextset(self) -> Optional[bool]: + """ + Move to the next result set if `execute()` returned more than one. + """ self._iresult += 1 if self._iresult < len(self._results): self.pgresult = self._results[self._iresult] @@ -417,10 +439,16 @@ class Cursor(BaseCursor): self.close() def close(self) -> None: + """ + Close the current cursor and free associated resources. + """ self._closed = True self._reset() def execute(self, query: Query, vars: Optional[Params] = None) -> "Cursor": + """ + Execute a query or command to the database. + """ with self.connection.lock: self._start_query() self.connection._start_query() @@ -433,6 +461,9 @@ class Cursor(BaseCursor): def executemany( self, query: Query, vars_seq: Sequence[Params] ) -> "Cursor": + """ + Execute the same command with a sequence of input data. + """ with self.connection.lock: self._start_query() self.connection._start_query() @@ -462,10 +493,16 @@ class Cursor(BaseCursor): args: Optional[Params] = None, kwargs: Optional[Mapping[str, Any]] = None, ) -> Optional[Params]: + """ + Call a stored procedure to the database. + """ self.execute(self._callproc_sql(name, args)) return args def fetchone(self) -> Optional[Sequence[Any]]: + """ + Return the next record from the current recordset, `None` if not available. + """ self._check_result() rv = self._transformer.load_row(self._pos) if rv is not None: @@ -473,6 +510,11 @@ class Cursor(BaseCursor): return rv def fetchmany(self, size: int = 0) -> List[Sequence[Any]]: + """ + Return the next *size* records from the current recordset. + + *size* default to `!self.arraysize` if not specified. + """ self._check_result() if not size: size = self.arraysize @@ -492,6 +534,9 @@ class Cursor(BaseCursor): return rv def fetchall(self) -> List[Sequence[Any]]: + """ + Return all the remaining records from the current recordset. + """ return list(self) def __iter__(self) -> Iterator[Sequence[Any]]: @@ -507,6 +552,9 @@ class Cursor(BaseCursor): yield row def copy(self, statement: Query, vars: Optional[Params] = None) -> Copy: + """ + Initiate a :sql:`COPY` operation and return a `Copy` object to manage it. + """ with self.connection.lock: self._start_query() self.connection._start_query() @@ -641,6 +689,9 @@ class AsyncCursor(BaseCursor): async def copy( self, statement: Query, vars: Optional[Params] = None ) -> AsyncCopy: + """ + Initiate a :sql:`COPY` operation and return an `AsyncCopy` object. + """ async with self.connection.lock: self._start_query() await self.connection._start_query()