Browse Source
The extensions have yet to receive this treatment and CSS needs work, but for now this is fine.pull/6176/head
5 changed files with 440 additions and 4 deletions
@ -0,0 +1,199 @@ |
|||
from sphinx.util.docutils import SphinxDirective |
|||
from sphinx.locale import _ |
|||
from docutils import nodes |
|||
from sphinx import addnodes |
|||
|
|||
from collections import OrderedDict |
|||
import importlib |
|||
import inspect |
|||
import os |
|||
import re |
|||
|
|||
class attributetable(nodes.General, nodes.Element): |
|||
pass |
|||
|
|||
class attributetablecolumn(nodes.General, nodes.Element): |
|||
pass |
|||
|
|||
class attributetabletitle(nodes.TextElement): |
|||
pass |
|||
|
|||
class attributetableplaceholder(nodes.General, nodes.Element): |
|||
pass |
|||
|
|||
def visit_attributetable_node(self, node): |
|||
self.body.append('<div class="py-attribute-table" data-move-to-id="%s">' % node['python-class']) |
|||
|
|||
def visit_attributetablecolumn_node(self, node): |
|||
self.body.append(self.starttag(node, 'div', CLASS='py-attribute-table-column')) |
|||
|
|||
def visit_attributetabletitle_node(self, node): |
|||
self.body.append(self.starttag(node, 'span')) |
|||
|
|||
def depart_attributetable_node(self, node): |
|||
self.body.append('</div>') |
|||
|
|||
def depart_attributetablecolumn_node(self, node): |
|||
self.body.append('</div>') |
|||
|
|||
def depart_attributetabletitle_node(self, node): |
|||
self.body.append('</span>') |
|||
|
|||
_name_parser_regex = re.compile(r'(?P<module>[\w.]+\.)?(?P<name>\w+)') |
|||
|
|||
class PyAttributeTable(SphinxDirective): |
|||
has_content = False |
|||
required_arguments = 1 |
|||
optional_arguments = 0 |
|||
final_argument_whitespace = False |
|||
option_spec = {} |
|||
|
|||
def parse_name(self, content): |
|||
path, name = _name_parser_regex.match(content).groups() |
|||
if path: |
|||
modulename = path.rstrip('.') |
|||
else: |
|||
modulename = self.env.temp_data.get('autodoc:module') |
|||
if not modulename: |
|||
modulename = self.env.ref_context.get('py:module') |
|||
if modulename is None: |
|||
raise RuntimeError('modulename somehow None for %s in %s.' % (content, self.env.docname)) |
|||
|
|||
return modulename, name |
|||
|
|||
def run(self): |
|||
"""If you're curious on the HTML this is meant to generate: |
|||
|
|||
<div class="py-attribute-table"> |
|||
<div class="py-attribute-table-column"> |
|||
<span>_('Attributes')</span> |
|||
<ul> |
|||
<li><a href="..."></li> |
|||
</ul> |
|||
</div> |
|||
<div class="py-attribute-table-column"> |
|||
<span>_('Coroutines')</span> |
|||
<ul> |
|||
<li><a href="..."></li> |
|||
</ul> |
|||
</div> |
|||
<div class="py-attribute-table-column"> |
|||
<span>_('Methods')</span> |
|||
<ul> |
|||
<li><a href="..."></li> |
|||
</ul> |
|||
</div> |
|||
... |
|||
</div> |
|||
|
|||
However, since this requires the tree to be complete |
|||
and parsed, it'll need to be done at a different stage and then |
|||
replaced. |
|||
""" |
|||
content = self.arguments[0].strip() |
|||
node = attributetableplaceholder('') |
|||
modulename, name = self.parse_name(content) |
|||
node['python-module'] = modulename |
|||
node['python-class'] = name |
|||
node['python-full-name'] = '%s.%s' % (modulename, name) |
|||
return [node] |
|||
|
|||
def build_lookup_table(env): |
|||
# Given an environment, load up a lookup table of |
|||
# full-class-name: objects |
|||
result = {} |
|||
domain = env.domains['py'] |
|||
|
|||
ignored = { |
|||
'data', 'exception', 'module', 'class', |
|||
} |
|||
for (fullname, (docname, objtype)) in domain.objects.items(): |
|||
if objtype in ignored: |
|||
continue |
|||
|
|||
classname, _, child = fullname.rpartition('.') |
|||
try: |
|||
result[classname].append(child) |
|||
except KeyError: |
|||
result[classname] = [child] |
|||
|
|||
return result |
|||
|
|||
def process_attributetable(app, doctree, fromdocname): |
|||
env = app.builder.env |
|||
|
|||
lookup = build_lookup_table(env) |
|||
for node in doctree.traverse(attributetableplaceholder): |
|||
modulename, classname, fullname = node['python-module'], node['python-class'], node['python-full-name'] |
|||
groups = get_class_results(lookup, modulename, classname, fullname) |
|||
table = attributetable('') |
|||
for label, subitems in groups.items(): |
|||
if not subitems: |
|||
continue |
|||
table.append(class_results_to_node(label, subitems)) |
|||
|
|||
table['python-class'] = fullname |
|||
|
|||
if not table: |
|||
node.replace_self([]) |
|||
else: |
|||
node.replace_self([table]) |
|||
|
|||
def get_class_results(lookup, modulename, name, fullname): |
|||
module = importlib.import_module(modulename) |
|||
cls_dict = getattr(module, name).__dict__ |
|||
|
|||
groups = OrderedDict([ |
|||
('Attributes', []), |
|||
('Coroutines', []), |
|||
('Methods', []), |
|||
('Decorators', []), |
|||
]) |
|||
|
|||
try: |
|||
members = lookup[fullname] |
|||
except KeyError: |
|||
return groups |
|||
|
|||
for attr in members: |
|||
attrlookup = '%s.%s' % (fullname, attr) |
|||
key = 'Attributes' |
|||
label = attr |
|||
|
|||
value = cls_dict.get(attr) |
|||
if value is not None: |
|||
doc = value.__doc__ or '' |
|||
if inspect.iscoroutinefunction(value) or doc.startswith('|coro|'): |
|||
key = 'Coroutines' |
|||
elif inspect.isfunction(value): |
|||
if doc.startswith(('A decorator', 'A shortcut decorator')): |
|||
# finicky but surprisingly consistent |
|||
key = 'Decorators' |
|||
else: |
|||
key = 'Methods' |
|||
|
|||
groups[key].append((attrlookup, label)) |
|||
|
|||
return groups |
|||
|
|||
def class_results_to_node(key, elements): |
|||
title = attributetabletitle(key, key) |
|||
ul = nodes.bullet_list('') |
|||
for fullname, label in elements: |
|||
ref = nodes.reference('', '', internal=True, |
|||
refuri='#' + fullname, |
|||
anchorname='', |
|||
*[nodes.Text(label)]) |
|||
para = addnodes.compact_paragraph('', '', ref) |
|||
item = nodes.list_item('', para) |
|||
ul.append(item) |
|||
|
|||
return attributetablecolumn('', title, ul) |
|||
|
|||
def setup(app): |
|||
app.add_directive('attributetable', PyAttributeTable) |
|||
app.add_node(attributetable, html=(visit_attributetable_node, depart_attributetable_node)) |
|||
app.add_node(attributetablecolumn, html=(visit_attributetablecolumn_node, depart_attributetablecolumn_node)) |
|||
app.add_node(attributetabletitle, html=(visit_attributetabletitle_node, depart_attributetabletitle_node)) |
|||
app.add_node(attributetableplaceholder) |
|||
app.connect('doctree-resolved', process_attributetable) |
Loading…
Reference in new issue