attributetable.depart_attributetablecolumn_node()   A
last analyzed

Complexity

Conditions 1

Size

Total Lines 2
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 2
dl 0
loc 2
rs 10
c 0
b 0
f 0
cc 1
nop 2
1
from sphinx.util.docutils import SphinxDirective
2
from sphinx.locale import _
3
from docutils import nodes
4
from sphinx import addnodes
5
6
from collections import OrderedDict, namedtuple
7
import importlib
8
import inspect
9
import os
10
import re
11
12
13
class attributetable(nodes.General, nodes.Element):
14
    pass
15
16
17
class attributetablecolumn(nodes.General, nodes.Element):
18
    pass
19
20
21
class attributetabletitle(nodes.TextElement):
22
    pass
23
24
25
class attributetableplaceholder(nodes.General, nodes.Element):
26
    pass
27
28
29
class attributetablebadge(nodes.TextElement):
30
    pass
31
32
33
class attributetable_item(nodes.Part, nodes.Element):
34
    pass
35
36
37
def visit_attributetable_node(self, node):
38
    class_ = node["python-class"]
39
    self.body.append(
40
        f'<div class="py-attribute-table" data-move-to-id="{class_}">'
41
    )
42
43
44
def visit_attributetablecolumn_node(self, node):
45
    self.body.append(
46
        self.starttag(node, "div", CLASS="py-attribute-table-column")
47
    )
48
49
50
def visit_attributetabletitle_node(self, node):
51
    self.body.append(self.starttag(node, "span"))
52
53
54
def visit_attributetablebadge_node(self, node):
55
    attributes = {
56
        "class": "py-attribute-table-badge",
57
        "title": node["badge-type"],
58
    }
59
    self.body.append(self.starttag(node, "span", **attributes))
60
61
62
def visit_attributetable_item_node(self, node):
63
    self.body.append(
64
        self.starttag(node, "li", CLASS="py-attribute-table-entry")
65
    )
66
67
68
def depart_attributetable_node(self, node):
69
    self.body.append("</div>")
70
71
72
def depart_attributetablecolumn_node(self, node):
73
    self.body.append("</div>")
74
75
76
def depart_attributetabletitle_node(self, node):
77
    self.body.append("</span>")
78
79
80
def depart_attributetablebadge_node(self, node):
81
    self.body.append("</span>")
82
83
84
def depart_attributetable_item_node(self, node):
85
    self.body.append("</li>")
86
87
88
_name_parser_regex = re.compile(r"(?P<module>[\w.]+\.)?(?P<name>\w+)")
89
90
91
class PyAttributeTable(SphinxDirective):
92
    has_content = False
93
    required_arguments = 1
94
    optional_arguments = 0
95
    final_argument_whitespace = False
96
    option_spec = {}
97
98
    def parse_name(self, content):
99
        path, name = _name_parser_regex.match(content).groups()
100
        if path:
101
            modulename = path.rstrip(".")
102
        else:
103
            modulename = self.env.temp_data.get("autodoc:module")
104
            if not modulename:
105
                modulename = self.env.ref_context.get("py:module")
106
        if modulename is None:
107
            raise RuntimeError(
108
                "modulename somehow None for %s in %s."
109
                % (content, self.env.docname)
110
            )
111
112
        return modulename, name
113
114
    def run(self):
115
        """If you're curious on the HTML this is meant to generate:
116
        <div class="py-attribute-table">
117
            <div class="py-attribute-table-column">
118
                <span>_('Attributes')</span>
119
                <ul>
120
                    <li>
121
                        <a href="...">
122
                    </li>
123
                </ul>
124
            </div>
125
            <div class="py-attribute-table-column">
126
                <span>_('Methods')</span>
127
                <ul>
128
                    <li>
129
                        <a href="..."></a>
130
                        <span class="py-attribute-badge" title="decorator">D</span>
131
                    </li>
132
                </ul>
133
            </div>
134
        </div>
135
        However, since this requires the tree to be complete
136
        and parsed, it'll need to be done at a different stage and then
137
        replaced.
138
        """
139
        content = self.arguments[0].strip()
140
        node = attributetableplaceholder("")
141
        modulename, name = self.parse_name(content)
142
        node["python-doc"] = self.env.docname
143
        node["python-module"] = modulename
144
        node["python-class"] = name
145
        node["python-full-name"] = f"{modulename}.{name}"
146
        return [node]
147
148
149
def build_lookup_table(env):
150
    # Given an environment, load up a lookup table of
151
    # full-class-name: objects
152
    result = {}
153
    domain = env.domains["py"]
154
155
    ignored = {
156
        "data",
157
        "exception",
158
        "module",
159
        "class",
160
    }
161
162
    for (fullname, _, objtype, docname, _, _) in domain.get_objects():
163
        if objtype in ignored:
164
            continue
165
166
        classname, _, child = fullname.rpartition(".")
167
        try:
168
            result[classname].append(child)
169
        except KeyError:
170
            result[classname] = [child]
171
172
    return result
173
174
175
TableElement = namedtuple("TableElement", "fullname label badge")
176
177
178
def process_attributetable(app, doctree, fromdocname):
179
    env = app.builder.env
180
181
    lookup = build_lookup_table(env)
182
    for node in doctree.traverse(attributetableplaceholder):
183
        modulename, classname, fullname = (
184
            node["python-module"],
185
            node["python-class"],
186
            node["python-full-name"],
187
        )
188
        groups = get_class_results(lookup, modulename, classname, fullname)
189
        table = attributetable("")
190
        for label, subitems in groups.items():
191
            if not subitems:
192
                continue
193
            table.append(
194
                class_results_to_node(
195
                    label, sorted(subitems, key=lambda c: c.label)
196
                )
197
            )
198
199
        table["python-class"] = fullname
200
201
        node.replace_self([table] if table else [])
202
203
204
def get_class_results(lookup, modulename, name, fullname):
205
    module = importlib.import_module(modulename)
206
    cls = getattr(module, name)
207
208
    groups = OrderedDict(
209
        [
210
            (_("Attributes"), []),
211
            (_("Methods"), []),
212
        ]
213
    )
214
215
    try:
216
        members = lookup[fullname]
217
    except KeyError:
218
        return groups
219
220
    for attr in members:
221
        attrlookup = f"{fullname}.{attr}"
222
        key = _("Attributes")
223
        badge = None
224
        label = attr
225
        value = None
226
227
        for base in cls.__mro__:
228
            value = base.__dict__.get(attr)
229
            if value is not None:
230
                break
231
232
        if value is not None:
233
            doc = value.__doc__ or ""
234
            if inspect.iscoroutinefunction(value) or doc.startswith("|coro|"):
235
                key = _("Methods")
236
                badge = attributetablebadge("async", "async")
237
                badge["badge-type"] = _("coroutine")
238
            elif isinstance(value, classmethod):
239
                key = _("Methods")
240
                label = f"{name}.{attr}"
241
                badge = attributetablebadge("cls", "cls")
242
                badge["badge-type"] = _("classmethod")
243
            elif inspect.isfunction(value) or isinstance(value, staticmethod):
244
                if doc.startswith(
245
                    ("A decorator", "A shortcut decorator")
246
                ) or label in ("event", "loop"):
247
                    # finicky but surprisingly consistent
248
                    badge = attributetablebadge("@", "@")
249
                    badge["badge-type"] = _("decorator")
250
                    key = _("Methods")
251
                else:
252
                    key = _("Methods")
253
                    badge = attributetablebadge("def", "def")
254
                    badge["badge-type"] = _("method")
255
256
        groups[key].append(
257
            TableElement(fullname=attrlookup, label=label, badge=badge)
258
        )
259
260
    return groups
261
262
263
def class_results_to_node(key, elements):
264
    title = attributetabletitle(key, key)
265
    ul = nodes.bullet_list("")
266
    for element in elements:
267
        ref = nodes.reference(
268
            "",
269
            "",
270
            internal=True,
271
            refuri="#" + element.fullname,
272
            anchorname="",
273
            *[nodes.Text(element.label)],
274
        )
275
        para = addnodes.compact_paragraph("", "", ref)
276
        if element.badge is not None:
277
            ul.append(attributetable_item("", element.badge, para))
278
        else:
279
            ul.append(attributetable_item("", para))
280
281
    return attributetablecolumn("", title, ul)
282
283
284
def setup(app):
285
    app.add_directive("attributetable", PyAttributeTable)
286
    app.add_node(
287
        attributetable,
288
        html=(visit_attributetable_node, depart_attributetable_node),
289
    )
290
    app.add_node(
291
        attributetablecolumn,
292
        html=(
293
            visit_attributetablecolumn_node,
294
            depart_attributetablecolumn_node,
295
        ),
296
    )
297
    app.add_node(
298
        attributetabletitle,
299
        html=(visit_attributetabletitle_node, depart_attributetabletitle_node),
300
    )
301
    app.add_node(
302
        attributetablebadge,
303
        html=(visit_attributetablebadge_node, depart_attributetablebadge_node),
304
    )
305
    app.add_node(
306
        attributetable_item,
307
        html=(visit_attributetable_item_node, depart_attributetable_item_node),
308
    )
309
    app.add_node(attributetableplaceholder)
310
    app.connect("doctree-resolved", process_attributetable)
311