From 250a28702b4c6709009603ddaa0a4d6d3689d104 Mon Sep 17 00:00:00 2001 From: Daniele Varrazzo Date: Tue, 24 Nov 2020 03:13:35 +0000 Subject: [PATCH] Fix docs generation to avoid getting confused by __module__ rewritten Autodoc features which need the original module get confused, so e.g. pq enum members or Notify attribute docs are lost if not fixed. --- docs/connection.rst | 2 +- docs/errors.rst | 5 +-- docs/lib/pg3_docs.py | 82 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 84 insertions(+), 5 deletions(-) diff --git a/docs/connection.rst b/docs/connection.rst index 38cfed981..bc40a4b78 100644 --- a/docs/connection.rst +++ b/docs/connection.rst @@ -43,7 +43,7 @@ The `!Connection` class .. seealso:: - the list of `the accepted connection parameters`__ - - the `environment varialbes`__ affecting connection + - the `environment variables`__ affecting connection .. __: https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-PARAMKEYWORDS .. __: https://www.postgresql.org/docs/current/libpq-envars.html diff --git a/docs/errors.rst b/docs/errors.rst index 436efa4d7..59d9900e9 100644 --- a/docs/errors.rst +++ b/docs/errors.rst @@ -18,14 +18,11 @@ This module exposes objects to represent and examine database errors. callback functions registered with `~psycopg3.Connection.add_notice_handler()`. - All the information available from the `PQresultErrorField()`__ function + All the information available from the :pq:`PQresultErrorField()` function are exposed as attributes by the object. For instance the `!severity` attribute returns the `!PG_DIAG_SEVERITY` code. Please refer to the PostgreSQL documentation for the meaning of all the attributes. - .. __: https://www.postgresql.org/docs/current/static/libpq-exec.html - #LIBPQ-PQRESULTERRORFIELD - The attributes available are: .. attribute:: diff --git a/docs/lib/pg3_docs.py b/docs/lib/pg3_docs.py index cdb219ff0..e028e98af 100644 --- a/docs/lib/pg3_docs.py +++ b/docs/lib/pg3_docs.py @@ -4,6 +4,12 @@ Customisation for docs generation. # Copyright (C) 2020 The Psycopg Team +import os +import re +import importlib +from typing import Dict +from collections import deque + def process_docstring(app, what, name, obj, options, lines): pass @@ -27,3 +33,79 @@ def setup(app): app.connect("autodoc-process-docstring", process_docstring) app.connect("autodoc-process-signature", process_signature) app.connect("autodoc-before-process-signature", before_process_signature) + + import psycopg3 # type: ignore + + recover_defined_module(psycopg3) + monkeypatch_autodoc() + + +# Classes which may have __module__ overwritten +recovered_classes: Dict[type, str] = {} + + +def recover_defined_module(m): + """ + Find the module where classes with __module__ attribute hacked were defined. + + Autodoc will get confused and will fail to inspect attribute docstrings + (e.g. from enums and named tuples). + + Save the classes recovered in `recovered_classes`, to be used by + `monkeypatch_autodoc()`. + + """ + mdir = os.path.split(m.__file__)[0] + for fn in walk_modules(mdir): + assert fn.startswith(mdir) + modname = os.path.splitext(fn[len(mdir) + 1 :])[0].replace("/", ".") + modname = f"{m.__name__}.{modname}" + with open(fn) as f: + classnames = re.findall(r"^class\s+([^(:]+)", f.read(), re.M) + for cls in classnames: + cls = deep_import(f"{modname}.{cls}") + if cls.__module__ != modname: + recovered_classes[cls] = modname + + +def monkeypatch_autodoc(): + """ + Patch autodoc in order to use information found by `recover_defined_module`. + """ + from sphinx.ext.autodoc import Documenter + + orig_get_real_modname = Documenter.get_real_modname + + def fixed_get_real_modname(self): + if self.object in recovered_classes: + return recovered_classes[self.object] + return orig_get_real_modname(self) + + Documenter.get_real_modname = fixed_get_real_modname + + +def walk_modules(d): + for root, dirs, files in os.walk(d): + for f in files: + if f.endswith(".py"): + yield f"{root}/{f}" + + +def deep_import(name): + parts = deque(name.split(".")) + seen = [] + if not parts: + raise ValueError("name must be a dot-separated name") + + seen.append(parts.popleft()) + thing = importlib.import_module(seen[-1]) + while parts: + attr = parts.popleft() + seen.append(attr) + + if hasattr(thing, attr): + thing = getattr(thing, attr) + else: + thing = importlib.import_module(".".join(seen)) + + return thing -- 2.47.3