#sqlstate-exceptions table p {
margin: 0;
}
+
+/* less space between enum values */
+dl.attribute {
+ margin-bottom: 1rem;
+}
"sphinx.ext.intersphinx",
"sql_role",
"pg3_docs",
+ "libpq_docs",
]
# Add any paths that contain templates here, relative to this directory.
"py": ("https://docs.python.org/3", None),
"pg2": ("https://www.psycopg.org/docs/", None),
}
+
+autodoc_member_order = "bysource"
+
+# PostgreSQL docs version to link libpq functions to
+libpq_docs_version = "13"
cursor
sql
errors
+ pq
from_pg2
--- /dev/null
+"""
+Sphinx plugin to link to the libpq documentation.
+
+Add the ``:pq:`` role, to create a link to a libpq function, e.g. ::
+
+ :pq:`PQlibVersion()`
+
+will link to::
+
+ https://www.postgresql.org/docs/current/libpq-misc.html #LIBPQ-PQLIBVERSION
+
+"""
+
+# Copyright (C) 2020 The Psycopg Team
+
+import logging
+import urllib.request
+from pathlib import Path
+from functools import lru_cache
+from html.parser import HTMLParser
+
+from docutils import nodes, utils
+from docutils.parsers.rst import roles
+
+logger = logging.getLogger("sphinx.libpq_docs")
+
+
+class LibpqParser(HTMLParser):
+ def __init__(self, data, version="current"):
+ super().__init__()
+ self.data = data
+ self.version = version
+
+ self.section_id = None
+ self.varlist_id = None
+ self.in_term = False
+ self.in_func = False
+
+ def handle_starttag(self, tag, attrs):
+ if tag == "sect1":
+ self.handle_sect1(tag, attrs)
+ elif tag == "varlistentry":
+ self.handle_varlistentry(tag, attrs)
+ elif tag == "term":
+ self.in_term = True
+ elif tag == "function":
+ self.in_func = True
+
+ def handle_endtag(self, tag):
+ if tag == "term":
+ self.in_term = False
+ elif tag == "function":
+ self.in_func = False
+
+ def handle_data(self, data):
+ if not (self.in_term and self.in_func):
+ return
+
+ self.add_function(data)
+
+ def handle_sect1(self, tag, attrs):
+ attrs = dict(attrs)
+ if "id" in attrs:
+ self.section_id = attrs["id"]
+
+ def handle_varlistentry(self, tag, attrs):
+ attrs = dict(attrs)
+ if "id" in attrs:
+ self.varlist_id = attrs["id"]
+
+ def add_function(self, func_name):
+ self.data[func_name] = self.get_func_url()
+
+ def get_func_url(self):
+ assert self.section_id, "<sect1> tag not found"
+ assert self.varlist_id, "<varlistentry> tag not found"
+ return self._url_pattern.format(
+ version=self.version,
+ section=self.section_id,
+ func_id=self.varlist_id.upper(),
+ )
+
+ _url_pattern = (
+ "https://www.postgresql.org/docs/{version}/{section}.html#{func_id}"
+ )
+
+
+class LibpqReader:
+ # must be set before using the rest of the class.
+ app = None
+
+ _url_pattern = (
+ "https://raw.githubusercontent.com/postgres/postgres/REL_{ver}_STABLE"
+ "/doc/src/sgml/libpq.sgml"
+ )
+
+ data = None
+
+ def get_url(self, func):
+ if not self.data:
+ self.parse()
+
+ return self.data[func]
+
+ def parse(self):
+ if not self.local_file.exists():
+ self.download()
+
+ logger.info("parsing libpq docs from %s", self.local_file)
+ self.data = {}
+ parser = LibpqParser(self.data, version=self.version)
+ with self.local_file.open("r") as f:
+ parser.feed(f.read())
+
+ def download(self):
+ logger.info("downloading postgres libpq docs from %s", self.sgml_url)
+ data = urllib.request.urlopen(self.sgml_url).read()
+ with self.local_file.open("wb") as f:
+ f.write(data)
+
+ @property
+ def local_file(self):
+ return Path(self.app.doctreedir) / f"libpq-{self.version}.sgml"
+
+ @property
+ def sgml_url(self):
+ return self._url_pattern.format(ver=self.version)
+
+ @property
+ def version(self):
+ return self.app.config.libpq_docs_version
+
+
+@lru_cache
+def get_reader():
+ return LibpqReader()
+
+
+def pq_role(name, rawtext, text, lineno, inliner, options={}, content=[]):
+ reader = get_reader()
+ if "(" in text:
+ func, noise = text.split("(", 1)
+ noise = "(" + noise
+ else:
+ func = text
+ noise = ""
+
+ try:
+ url = reader.get_url(func)
+ except KeyError:
+ msg = inliner.reporter.warning(
+ f"function {func} not found in libpq {reader.version} docs"
+ )
+ prb = inliner.problematic(rawtext, rawtext, msg)
+ return [prb], [msg]
+
+ text = utils.unescape(text)
+
+ the_nodes = []
+ the_nodes.append(nodes.reference(func, func, refuri=url))
+ if noise:
+ the_nodes.append(nodes.Text(noise))
+
+ return [nodes.literal("", "", *the_nodes, **options)], []
+
+
+def setup(app):
+ app.add_config_value("libpq_docs_version", "13", "html")
+ roles.register_local_role("pq", pq_role)
+ get_reader().app = app
def before_process_signature(app, obj, bound_method):
- # Drop "return: None" from the function signatures
ann = getattr(obj, "__annotations__", {})
- if "return" in ann and ann["return"] is None:
- del ann["return"]
+ if "return" in ann:
+ # Drop "return: None" from the function signatures
+ if ann["return"] is None:
+ del ann["return"]
+ elif ann["return"] == "PGcancel":
+ ann["return"] = "psycopg3.pq.PGcancel"
def process_signature(
--- /dev/null
+`psycopg3.pq` -- Libpq wrapper module
+=====================================
+
+.. index::
+ single: libpq
+
+.. module:: psycopg3.pq
+
+``psycopg3`` is built around the libpq_, the PostgreSQL client library, which
+performs most of the network communications and returns query results in C
+structures.
+
+.. _libpq: https://www.postgresql.org/docs/current/libpq.html
+
+The low-level functions of the library are exposed by the objects in the
+`!psycopg3.pq` module.
+
+
+.. _pq-impl:
+
+``pq`` module implementations
+-----------------------------
+
+There are actually several implementations of the module, all offering the
+same interface. Current implementations are:
+
+- ``python``: a pure-python implementation, implemented using the `ctypes`
+ module. It is less performing than the others, but it doesn't need a C
+ compiler to install. It requires the libpq installed in the system.
+
+- ``c``: a C implementation of the libpq wrapper (more precisely, implemented
+ in Cython_). It is much better performing than the ``python``
+ implementation, however it requires development packages installed on the
+ client machine. It can be installed using the ``c`` extra, i.e. running
+ ``pip install psycopg3[c]``.
+
+- ``binary``: a pre-compiled C implementation, bundled with all the required
+ libraries. It is the easiest option to deal with, fast to install and it
+ should require no development tool or client library, however it may be not
+ available for every platform. You can install it using the ``binary`` extra,
+ i.e. running ``pip install psycopg3[binary]``.
+
+.. _Cython: https://cython.org/
+
+The implementation currently used is available in the `~psycopg3.pq.__impl__`
+module constant.
+
+At import, ``psycopg3`` will try to use the best implementation available and
+will fail if none is usable. You can force the use of a specific
+implementation by exporting the env var :envvar:`PSYCOPG3_IMPL`: importing the
+library will fail if the requested implementation is not available.
+
+
+Module content
+--------------
+
+.. autodata:: __impl__
+ :annotation: str:
+
+ The choice of implementation is automatic but can be forced setting the
+ :envvar:`PSYCOPG3_IMPL` env var.
+
+
+.. autofunction:: version
+
+ .. seealso:: the :pq:`PQlibVersion()` function
+
+
+.. autofunction:: error_message
+
+
+Objects
+-------
+
+TODO: finish documentation
+
+.. autoclass:: PGconn()
+ :members:
+
+.. autoclass:: PGresult()
+.. autoclass:: Conninfo
+.. autoclass:: Escaping
+
+.. autoclass:: PGcancel()
+ :members:
+
+
+Enumerations
+------------
+
+.. autoclass:: ConnStatus
+ :members:
+
+ Other possible status are only seen during the connection phase.
+
+ .. seealso:: :pq:`PQstatus()` returns this value.
+
+
+.. autoclass:: PollingStatus
+ :members:
+
+ .. seealso:: :pq:`PQconnectPoll` for a description of these states.
+
+
+.. autoclass:: TransactionStatus
+ :members:
+
+ .. seealso:: :pq:`PQtransactionStatus` for a description of these states.
+
+
+.. autoclass:: Format
+ :members:
+
+
+.. autoclass:: ExecStatus
+ :members:
+
+ .. seealso:: :pq:`PQresultStatus` for a description of these states.
+
+
+.. autoclass:: DiagnosticField
+
+ Available attributes:
+
+ .. attribute::
+ SEVERITY
+ SEVERITY_NONLOCALIZED
+ SQLSTATE
+ MESSAGE_PRIMARY
+ MESSAGE_DETAIL
+ MESSAGE_HINT
+ STATEMENT_POSITION
+ INTERNAL_POSITION
+ INTERNAL_QUERY
+ CONTEXT
+ SCHEMA_NAME
+ TABLE_NAME
+ COLUMN_NAME
+ DATATYPE_NAME
+ CONSTRAINT_NAME
+ SOURCE_FILE
+ SOURCE_LINE
+ SOURCE_FUNCTION
+
+ .. seealso:: :pq:`PQresultErrorField` for a description of these values.
+
+
+.. autoclass:: Ping
+ :members:
+
+ .. seealso:: :pq:`PQpingParams` for a description of these values.
logger = logging.getLogger(__name__)
__impl__: str
+"""The currently loaded implementation of the `!psycopg3.pq` package.
+
+Possible values include ``python``, ``c``, ``binary``.
+"""
+
version: Callable[[], int]
PGconn: Type[proto.PGconn]
PGresult: Type[proto.PGresult]
Conninfo: Type[proto.Conninfo]
Escaping: Type[proto.Escaping]
+PGcancel: Type[proto.PGcancel]
def import_from_libpq() -> None:
try to import the best implementation available.
"""
# import these names into the module on success as side effect
- global __impl__, version, PGconn, PGresult, Conninfo, Escaping
+ global __impl__, version, PGconn, PGresult, Conninfo, Escaping, PGcancel
impl = os.environ.get("PSYCOPG3_IMPL", "").lower()
module = None
PGresult = module.PGresult
Conninfo = module.Conninfo
Escaping = module.Escaping
+ PGcancel = module.PGcancel
elif impl:
raise ImportError(f"requested pq impementation '{impl}' unknown")
else:
class ConnStatus(IntEnum):
+ """
+ Current status of the connection.
+ """
+
OK = 0
+ """The connection is in a working state."""
BAD = auto()
+ """The connection is closed."""
+
STARTED = auto()
MADE = auto()
AWAITING_RESPONSE = auto()
class PollingStatus(IntEnum):
+ """
+ The status of the socket during a connection.
+
+ If ``READING`` or ``WRITING`` you may select before polling again.
+ """
+
FAILED = 0
+ """Connection attempt failed."""
READING = auto()
+ """Will have to wait before reading new data."""
WRITING = auto()
+ """Will have to wait before writing new data."""
OK = auto()
+ """Connection completed."""
+
ACTIVE = auto()
class ExecStatus(IntEnum):
+ """
+ The status of a command.
+ """
+
EMPTY_QUERY = 0
+ """The string sent to the server was empty."""
+
COMMAND_OK = auto()
+ """Successful completion of a command returning no data."""
+
TUPLES_OK = auto()
+ """
+ Successful completion of a command returning data (such as a SELECT or SHOW).
+ """
+
COPY_OUT = auto()
+ """Copy Out (from server) data transfer started."""
+
COPY_IN = auto()
+ """Copy In (to server) data transfer started."""
+
BAD_RESPONSE = auto()
+ """The server's response was not understood."""
+
NONFATAL_ERROR = auto()
+ """A nonfatal error (a notice or warning) occurred."""
+
FATAL_ERROR = auto()
+ """A fatal error occurred."""
+
COPY_BOTH = auto()
+ """
+ Copy In/Out (to and from server) data transfer started.
+
+ This feature is currently used only for streaming replication, so this
+ status should not occur in ordinary applications.
+ """
+
SINGLE_TUPLE = auto()
+ """
+ The PGresult contains a single result tuple from the current command.
+
+ This status occurs only when single-row mode has been selected for the
+ query.
+ """
class TransactionStatus(IntEnum):
+ """
+ The transaction status of a connection.
+ """
+
IDLE = 0
+ """Connection ready, no transaction active."""
+
ACTIVE = auto()
+ """A command is in progress."""
+
INTRANS = auto()
+ """Connection idle in an open transaction."""
+
INERROR = auto()
+ """An error happened in the current transaction."""
+
UNKNOWN = auto()
+ """Unknown connection state, broken connection."""
class Ping(IntEnum):
+ """Response from a ping attempt."""
+
OK = 0
+ """
+ The server is running and appears to be accepting connections.
+ """
+
REJECT = auto()
+ """
+ The server is running but is in a state that disallows connections.
+ """
+
NO_RESPONSE = auto()
+ """
+ The server could not be contacted.
+ """
+
NO_ATTEMPT = auto()
+ """
+ No attempt was made to contact the server.
+ """
class DiagnosticField(IntEnum):
+ """
+ Fields in an error report.
+ """
+
# from postgres_ext.h
SEVERITY = ord("S")
SEVERITY_NONLOCALIZED = ord("V")
class Format(IntEnum):
+ """
+ The format of a query argument or return value.
+ """
+
TEXT = 0
+ """Text parameter."""
BINARY = 1
+ """Binary parameter."""
def version() -> int:
+ """Return the version number of the libpq currently loaded."""
return impl.PQlibVersion()
class PGconn:
+ """
+ Python representation of a libpq connection.
+ """
+
__slots__ = (
"pgconn_ptr",
"notice_handler",
return rv
def get_cancel(self) -> "PGcancel":
+ """
+ Create an object with the information needed to cancel a command.
+
+ See :pq:`PQgetCancel` for details.
+ """
rv = impl.PQgetCancel(self.pgconn_ptr)
if not rv:
raise PQerror("couldn't create cancel object")
class PGresult:
+ """
+ Python representation of a libpq result.
+ """
+
__slots__ = ("pgresult_ptr",)
def __init__(self, pgresult_ptr: impl.PGresult_struct):
class PGcancel:
+ """
+ Token to cancel the current operation on a connection.
+
+ Created by `PGconn.get_cancel()`.
+ """
+
__slots__ = ("pgcancel_ptr",)
def __init__(self, pgcancel_ptr: impl.PGcancel_struct):
self.free()
def free(self) -> None:
+ """
+ Free the data structure created by PQgetCancel.
+
+ Automatically invoked by `!__del__()`.
+
+ See :pq:`PQfreeCancel` for details.
+ """
self.pgcancel_ptr, p = None, self.pgcancel_ptr
if p:
impl.PQfreeCancel(p)
def cancel(self) -> None:
+ """Requests that the server abandon processing of the current command.
+
+ See :pq:`PQcancel()` for details.
+ """
buf = create_string_buffer(256)
res = impl.PQcancel(
self.pgcancel_ptr, pointer(buf), len(buf) # type: ignore
class Conninfo:
+ """
+ Utility object to manipulate connection strings.
+ """
+
@classmethod
def get_defaults(cls) -> List[ConninfoOption]:
opts = impl.PQconndefaults()
class Escaping:
+ """
+ Utility object to escape strings for SQL interpolation.
+ """
+
def __init__(self, conn: Optional[PGconn] = None):
self.conn = conn
import string
from typing import Any, Iterator, List, Optional, Sequence, Union
+from typing import TYPE_CHECKING
from .pq import Escaping, Format
from .proto import AdaptContext
+if TYPE_CHECKING:
+ import psycopg3
+
def quote(obj: Any, context: AdaptContext = None) -> str:
"""
"""
- def __init__(self, name: str = "", format: Format = Format.TEXT):
+ def __init__(
+ self, name: str = "", format: "psycopg3.pq.Format" = Format.TEXT
+ ):
super().__init__(name)
if not isinstance(name, str):
raise TypeError(f"expected string as name, got {name!r}")