Skip to content

Commit e5ecfd7

Browse files
committed
Add aiter directive (docs header)
1 parent c8a44c4 commit e5ecfd7

File tree

3 files changed

+212
-1
lines changed

3 files changed

+212
-1
lines changed

docs/_extensions/aiter.py

Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
from __future__ import annotations
2+
3+
import re
4+
5+
from docutils import nodes
6+
from docutils.parsers.rst import directives
7+
from sphinx import addnodes
8+
from sphinx.application import Sphinx
9+
from sphinx.domains.python import PyFunction, PyMethod
10+
from sphinx.ext.autodoc import FunctionDocumenter, MethodDocumenter
11+
from sphinx.writers.html5 import HTML5Translator
12+
13+
14+
NAME_RE: re.Pattern[str] = re.compile(r"(?P<module>[\w.]+\.)?(?P<method>\w+)")
15+
PYTHON_DOC_STD: str = "https://docs.python.org/3/library/stdtypes.html"
16+
17+
18+
class hrnode(nodes.General, nodes.Element):
19+
pass
20+
21+
22+
class usagetable(nodes.General, nodes.Element):
23+
pass
24+
25+
26+
class aiter(nodes.General, nodes.Element):
27+
pass
28+
29+
30+
def visit_usagetable_node(self: HTML5Translator, node: usagetable):
31+
self.body.append(self.starttag(node, "div", CLASS="sig-usagetable"))
32+
33+
34+
def depart_usagetable_node(self: HTML5Translator, node: usagetable):
35+
self.body.append("</div>")
36+
37+
38+
def visit_aiterinfo_node(self: HTML5Translator, node: aiter):
39+
dot = "." if node.get("python-class-name", False) else ""
40+
41+
self.body.append(self.starttag(node, "span", CLASS="pre"))
42+
43+
self.body.append("<em>await </em>")
44+
self.body.append(self.starttag(node, "span", CLASS="sig-name"))
45+
self.body.append(f"{dot}{node['python-name']}()</span>")
46+
47+
self.body.append(self.starttag(node, "span"))
48+
self.body.append(" -> ")
49+
self.body.append("</span>")
50+
51+
list_ = f"{PYTHON_DOC_STD}#list"
52+
self.body.append(self.starttag(node, "a", href=list_))
53+
self.body.append("list")
54+
self.body.append("</a>")
55+
56+
# TODO: Type...
57+
self.body.append("[T]: ...")
58+
59+
self.body.append(self.starttag(node, "br"))
60+
61+
self.body.append("<em>async for</em> item in ")
62+
self.body.append(self.starttag(node, "span", CLASS="sig-name"))
63+
self.body.append(f"{dot}{node['python-name']}()")
64+
self.body.append("</span>")
65+
self.body.append(": ...")
66+
67+
68+
def depart_aiterinfo_node(self: HTML5Translator, node: aiter):
69+
self.body.append("</span>")
70+
71+
72+
def visit_hr_node(self: HTML5Translator, node: hrnode):
73+
self.body.append(self.starttag(node, "hr"))
74+
75+
76+
def depart_hr_node(self: HTML5Translator, node: hrnode):
77+
self.body.append("</hr>")
78+
79+
80+
def check_return(sig: str) -> bool:
81+
if not sig:
82+
return False
83+
84+
splat = sig.split("->")
85+
ret = splat[-1]
86+
87+
return "HTTPAsyncIterator" in ret
88+
89+
90+
class AiterPyF(PyFunction):
91+
option_spec = PyFunction.option_spec.copy()
92+
option_spec.update({"aiter": directives.flag})
93+
94+
def parse_name_(self, content: str) -> tuple[str | None, str]:
95+
match = NAME_RE.match(content)
96+
97+
if match is None:
98+
raise RuntimeError(f"content {content} somehow doesn't match regex in {self.env.docname}.")
99+
100+
path, name = match.groups()
101+
102+
if path:
103+
modulename = path.rstrip(".")
104+
else:
105+
modulename = self.env.temp_data.get("autodoc:module")
106+
if not modulename:
107+
modulename = self.env.ref_context.get("py:module")
108+
109+
return modulename, name
110+
111+
def get_signature_prefix(self, sig: str) -> list[nodes.Node]:
112+
mname, name = self.parse_name_(sig)
113+
114+
if "aiter" in self.options:
115+
node = aiter()
116+
node["python-fullname"] = f"{mname}.{name}"
117+
node["python-name"] = name
118+
node["python-module"] = mname
119+
120+
parent = usagetable("", node)
121+
return [parent, hrnode(), addnodes.desc_sig_keyword("", "async"), addnodes.desc_sig_space()]
122+
123+
return super().get_signature_prefix(sig)
124+
125+
126+
class AiterPyM(PyMethod):
127+
option_spec = PyMethod.option_spec.copy()
128+
option_spec.update({"aiter": directives.flag})
129+
130+
def parse_name_(self, content: str) -> tuple[str, str]:
131+
match = NAME_RE.match(content)
132+
133+
if match is None:
134+
raise RuntimeError(f"content {content} somehow doesn't match regex in {self.env.docname}.")
135+
136+
cls, name = match.groups()
137+
return cls, name
138+
139+
def get_signature_prefix(self, sig: str) -> list[nodes.Node]:
140+
cname, name = self.parse_name_(sig)
141+
142+
if "aiter" in self.options:
143+
node = aiter()
144+
node["python-name"] = name
145+
node["python-class-name"] = cname
146+
147+
parent = usagetable("", node)
148+
return [parent, hrnode(), addnodes.desc_sig_keyword("", "async"), addnodes.desc_sig_space()]
149+
150+
return super().get_signature_prefix(sig)
151+
152+
153+
class AiterFuncDocumenter(FunctionDocumenter):
154+
objtype = "function"
155+
priority = FunctionDocumenter.priority + 1
156+
157+
def add_directive_header(self, sig: str) -> None:
158+
super().add_directive_header(sig)
159+
160+
sourcename = self.get_sourcename()
161+
docs = self.object.__doc__ or ""
162+
163+
if docs.startswith("|aiter|") or check_return(sig):
164+
self.add_line(" :aiter:", sourcename)
165+
166+
167+
class AiterMethDocumenter(MethodDocumenter):
168+
objtype = "method"
169+
priority = MethodDocumenter.priority + 1
170+
171+
def add_directive_header(self, sig: str) -> None:
172+
super().add_directive_header(sig)
173+
174+
sourcename = self.get_sourcename()
175+
obj = self.parent.__dict__.get(self.object_name, self.object)
176+
177+
docs = obj.__doc__ or ""
178+
if docs.startswith("|aiter|") or check_return(sig):
179+
self.add_line(" :aiter:", sourcename)
180+
181+
182+
def setup(app: Sphinx) -> dict[str, bool]:
183+
app.setup_extension("sphinx.directives")
184+
app.setup_extension("sphinx.ext.autodoc")
185+
186+
app.add_directive_to_domain("py", "function", AiterPyF, override=True)
187+
app.add_directive_to_domain("py", "method", AiterPyM, override=True)
188+
189+
app.add_autodocumenter(AiterMethDocumenter, override=True)
190+
app.add_autodocumenter(AiterFuncDocumenter, override=True)
191+
192+
app.add_node(aiter, html=(visit_aiterinfo_node, depart_aiterinfo_node))
193+
app.add_node(usagetable, html=(visit_usagetable_node, depart_usagetable_node))
194+
app.add_node(hrnode, html=(visit_hr_node, depart_hr_node))
195+
196+
return {"parallel_read_safe": True}

docs/_static/custom.css

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,16 @@ a > code {
247247
border-color: var(--color-background-dim--light);
248248
}
249249

250+
.sig-usagetable {
251+
font-style: normal;
252+
margin-top: 0.5rem;
253+
padding-left: 1rem;
254+
font-size: 0.9em;
255+
font-weight: 400;
256+
border-left: 2px solid var(--color-links--light);
257+
line-height: 1.6;
258+
}
259+
250260
pre {
251261
color: var(--color-pre-primary--light) !important;
252262
background-color: var(--color-pre-background--light);
@@ -258,6 +268,10 @@ body.theme-dark {
258268
}
259269

260270
body.theme-dark {
271+
.sig-usagetable {
272+
border-left: 2px solid var(--color-links--dark)!important;
273+
}
274+
261275
code {
262276
color: #cb7f90!important;
263277
background-color: var(--color-background-dim--dark);

docs/conf.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
"hoverxref.extension",
5353
"sphinxcontrib_trio",
5454
"sphinx_wagtail_theme",
55+
"aiter",
5556
]
5657

5758
# Add any paths that contain templates here, relative to this directory.
@@ -73,7 +74,7 @@
7374

7475
html_theme_options = dict(
7576
project_name="Documentation",
76-
github_url = "https://github.com/PythonistaGuild/TwitchIO/tree/dev/3.0/docs/",
77+
github_url="https://github.com/PythonistaGuild/TwitchIO/tree/dev/3.0/docs/",
7778
logo="logo.png",
7879
logo_alt="TwitchIO",
7980
logo_height=120,

0 commit comments

Comments
 (0)