]> git.ipfire.org Git - thirdparty/psycopg.git/commitdiff
Fix docs generation to avoid getting confused by __module__ rewritten
authorDaniele Varrazzo <daniele.varrazzo@gmail.com>
Tue, 24 Nov 2020 03:13:35 +0000 (03:13 +0000)
committerDaniele Varrazzo <daniele.varrazzo@gmail.com>
Tue, 24 Nov 2020 03:21:01 +0000 (03:21 +0000)
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
docs/errors.rst
docs/lib/pg3_docs.py

index 38cfed9817f19729171954df857a9c6d7ae60989..bc40a4b788f6818a94a65028409bfc29cbb3f465 100644 (file)
@@ -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
index 436efa4d70b8f7fda773a60df7bdfe2b77d43732..59d9900e9695377bd4f82f884a19518a28cebdab 100644 (file)
@@ -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::
index cdb219ff0d3579de45a15da7242349a69d5192a6..e028e98af8fd7e46b6cd903b100df093427670ab 100644 (file)
@@ -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