From: Daniele Varrazzo Date: Mon, 23 Nov 2020 05:28:43 +0000 (+0000) Subject: Added initial docs for psycopg3.pq module X-Git-Tag: 3.0.dev0~325 X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=9f628158628382ef4ec3c5e8156b69ba0d433db6;p=thirdparty%2Fpsycopg.git Added initial docs for psycopg3.pq module --- diff --git a/docs/_static/psycopg.css b/docs/_static/psycopg.css index e4acd5b71..819326274 100644 --- a/docs/_static/psycopg.css +++ b/docs/_static/psycopg.css @@ -7,3 +7,8 @@ #sqlstate-exceptions table p { margin: 0; } + +/* less space between enum values */ +dl.attribute { + margin-bottom: 1rem; +} diff --git a/docs/conf.py b/docs/conf.py index 8228bc3b1..11820ac09 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -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" diff --git a/docs/index.rst b/docs/index.rst index 8e5a347ae..6127b29f0 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -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 index 000000000..5685a2703 --- /dev/null +++ b/docs/lib/libpq_docs.py @@ -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, " tag not found" + assert self.varlist_id, " 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 diff --git a/docs/lib/pg3_docs.py b/docs/lib/pg3_docs.py index cfb6d4f8e..ae73dbe60 100644 --- a/docs/lib/pg3_docs.py +++ b/docs/lib/pg3_docs.py @@ -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 index 000000000..bc6058ff1 --- /dev/null +++ b/docs/pq.rst @@ -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. diff --git a/psycopg3/psycopg3/pq/__init__.py b/psycopg3/psycopg3/pq/__init__.py index 8ebee38d5..f4233492e 100644 --- a/psycopg3/psycopg3/pq/__init__.py +++ b/psycopg3/psycopg3/pq/__init__.py @@ -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: diff --git a/psycopg3/psycopg3/pq/enums.py b/psycopg3/psycopg3/pq/enums.py index e692fc14e..c42645fd7 100644 --- a/psycopg3/psycopg3/pq/enums.py +++ b/psycopg3/psycopg3/pq/enums.py @@ -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.""" diff --git a/psycopg3/psycopg3/pq/pq_ctypes.py b/psycopg3/psycopg3/pq/pq_ctypes.py index da91410f3..30fb39ba6 100644 --- a/psycopg3/psycopg3/pq/pq_ctypes.py +++ b/psycopg3/psycopg3/pq/pq_ctypes.py @@ -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 diff --git a/psycopg3/psycopg3/sql.py b/psycopg3/psycopg3/sql.py index 249830ae8..580daf211 100644 --- a/psycopg3/psycopg3/sql.py +++ b/psycopg3/psycopg3/sql.py @@ -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}")