]> git.ipfire.org Git - thirdparty/psycopg.git/commitdiff
Added initial docs for psycopg3.pq module
authorDaniele Varrazzo <daniele.varrazzo@gmail.com>
Mon, 23 Nov 2020 05:28:43 +0000 (05:28 +0000)
committerDaniele Varrazzo <daniele.varrazzo@gmail.com>
Mon, 23 Nov 2020 05:30:13 +0000 (05:30 +0000)
docs/_static/psycopg.css
docs/conf.py
docs/index.rst
docs/lib/libpq_docs.py [new file with mode: 0644]
docs/lib/pg3_docs.py
docs/pq.rst [new file with mode: 0644]
psycopg3/psycopg3/pq/__init__.py
psycopg3/psycopg3/pq/enums.py
psycopg3/psycopg3/pq/pq_ctypes.py
psycopg3/psycopg3/sql.py

index e4acd5b71a86a72c1522a83bce9f322ec6e82852..819326274219813a315c5660e91cbdaa963ffc98 100644 (file)
@@ -7,3 +7,8 @@
 #sqlstate-exceptions table p {
   margin: 0;
 }
+
+/* less space between enum values */
+dl.attribute {
+  margin-bottom: 1rem;
+}
index 8228bc3b1d6f01eb1a193901fdf04e2680ba3367..11820ac096d3a479b540b9fbd2a2bc0defc4bf6e 100644 (file)
@@ -40,6 +40,7 @@ extensions = [
     "sphinx.ext.intersphinx",
     "sql_role",
     "pg3_docs",
+    "libpq_docs",
 ]
 
 # Add any paths that contain templates here, relative to this directory.
@@ -76,3 +77,8 @@ intersphinx_mapping = {
     "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"
index 8e5a347aebaa062af39e49d6332068921da6c8a0..6127b29f0be4a9c335d32fe1e66b6df2a009a988 100644 (file)
@@ -26,6 +26,7 @@ the COPY support.
     cursor
     sql
     errors
+    pq
     from_pg2
 
 
diff --git a/docs/lib/libpq_docs.py b/docs/lib/libpq_docs.py
new file mode 100644 (file)
index 0000000..5685a27
--- /dev/null
@@ -0,0 +1,170 @@
+"""
+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
index cfb6d4f8e1479440111fc4a13692d446e8a2184c..ae73dbe6057ef39d6590a0429d51f0e7c295d6cb 100644 (file)
@@ -10,10 +10,13 @@ def process_docstring(app, what, name, obj, options, lines):
 
 
 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(
diff --git a/docs/pq.rst b/docs/pq.rst
new file mode 100644 (file)
index 0000000..bc6058f
--- /dev/null
@@ -0,0 +1,151 @@
+`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.
index 8ebee38d58a568f3031634fab58bad56ad41ca53..f4233492ecbc83b03ad6a7f225d977fedd466070 100644 (file)
@@ -29,11 +29,17 @@ from . import proto
 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:
@@ -44,7 +50,7 @@ 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
@@ -93,6 +99,7 @@ def import_from_libpq() -> None:
         PGresult = module.PGresult
         Conninfo = module.Conninfo
         Escaping = module.Escaping
+        PGcancel = module.PGcancel
     elif impl:
         raise ImportError(f"requested pq impementation '{impl}' unknown")
     else:
index e692fc14ed2b99c0a7824b4c13acd62b12feb9cd..c42645fd7284064890b2491045d3b7cff3d074d7 100644 (file)
@@ -8,8 +8,15 @@ from enum import IntEnum, auto
 
 
 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()
@@ -24,42 +31,122 @@ class ConnStatus(IntEnum):
 
 
 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")
@@ -82,5 +169,11 @@ class DiagnosticField(IntEnum):
 
 
 class Format(IntEnum):
+    """
+    The format of a query argument or return value.
+    """
+
     TEXT = 0
+    """Text parameter."""
     BINARY = 1
+    """Binary parameter."""
index da91410f30079ac9b86e986c872af67907398eb8..30fb39ba6d542aa9bb4b17127773be6488a75b36 100644 (file)
@@ -40,6 +40,7 @@ logger = logging.getLogger("psycopg3")
 
 
 def version() -> int:
+    """Return the version number of the libpq currently loaded."""
     return impl.PQlibVersion()
 
 
@@ -60,6 +61,10 @@ def notice_receiver(
 
 
 class PGconn:
+    """
+    Python representation of a libpq connection.
+    """
+
     __slots__ = (
         "pgconn_ptr",
         "notice_handler",
@@ -492,6 +497,11 @@ class PGconn:
         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")
@@ -571,6 +581,10 @@ class PGconn:
 
 
 class PGresult:
+    """
+    Python representation of a libpq result.
+    """
+
     __slots__ = ("pgresult_ptr",)
 
     def __init__(self, pgresult_ptr: impl.PGresult_struct):
@@ -676,6 +690,12 @@ class PGresult:
 
 
 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):
@@ -685,11 +705,22 @@ class PGcancel:
         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
@@ -701,6 +732,10 @@ class PGcancel:
 
 
 class Conninfo:
+    """
+    Utility object to manipulate connection strings.
+    """
+
     @classmethod
     def get_defaults(cls) -> List[ConninfoOption]:
         opts = impl.PQconndefaults()
@@ -748,6 +783,10 @@ class Conninfo:
 
 
 class Escaping:
+    """
+    Utility object to escape strings for SQL interpolation.
+    """
+
     def __init__(self, conn: Optional[PGconn] = None):
         self.conn = conn
 
index 249830ae82ce56323afdfc10d64659cf97ccb95d..580daf211724fb0713c81fb69c91392ef62eeb15 100644 (file)
@@ -6,10 +6,14 @@ SQL composition utility module
 
 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:
     """
@@ -407,7 +411,9 @@ class Placeholder(Composable):
 
     """
 
-    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}")