1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185
|
# mypy: ignore-errors
"""
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::
file:///usr/share/doc/postgresql-doc-16/html/libpq-misc.html #LIBPQ-PQLIBVERSION
previously to:
https://www.postgresql.org/docs/current/libpq-misc.html #LIBPQ-PQLIBVERSION
"""
# Copyright (C) 2020 The Psycopg Team
import os
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):
if "id" in (attrs := dict(attrs)):
self.section_id = attrs["id"]
def handle_varlistentry(self, tag, attrs):
if "id" in (attrs := dict(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 = "file:///usr/share/doc/postgresql-doc-{version}/html/{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/"
_url_pattern += "{branch}/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):
if filename := os.environ.get("LIBPQ_DOCS_FILE"):
logger.info("reading postgres libpq docs from %s", filename)
with open(filename, "rb") as f:
data = f.read()
else:
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(branch=self.branch)
@property
def branch(self):
if self.version == "devel":
return "master"
return f"REL_{self.version}_STABLE"
@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=[]):
text = utils.unescape(text)
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]
# For a function f(), include the () in the signature for consistency
# with a normal `thing()`
if noise == "()":
func, noise = func + noise, ""
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", "17", "html")
roles.register_local_role("pq", pq_role)
get_reader().app = app
|