mirror of
https://github.com/RGBCube/GitHubWrapper
synced 2025-05-31 04:58:12 +00:00
Docs base plate?
This commit is contained in:
parent
359c483c66
commit
187f15f8bc
29 changed files with 3493 additions and 52 deletions
287
docs/extensions/attributetable.py
Normal file
287
docs/extensions/attributetable.py
Normal file
|
@ -0,0 +1,287 @@
|
|||
from __future__ import annotations
|
||||
import importlib
|
||||
import inspect
|
||||
import re
|
||||
from typing import Dict, List, NamedTuple, Optional, Tuple, Sequence, TYPE_CHECKING
|
||||
|
||||
from docutils import nodes
|
||||
from sphinx import addnodes
|
||||
from sphinx.application import Sphinx
|
||||
from sphinx.environment import BuildEnvironment
|
||||
from sphinx.locale import _
|
||||
from sphinx.util.docutils import SphinxDirective
|
||||
from sphinx.util.typing import OptionSpec
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .builder import DPYHTML5Translator
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
class attributetablebadge(nodes.TextElement):
|
||||
pass
|
||||
|
||||
|
||||
class attributetable_item(nodes.Part, nodes.Element):
|
||||
pass
|
||||
|
||||
|
||||
def visit_attributetable_node(self: DPYHTML5Translator, node: attributetable) -> None:
|
||||
class_ = node['python-class']
|
||||
self.body.append(f'<div class="py-attribute-table" data-move-to-id="{class_}">')
|
||||
|
||||
|
||||
def visit_attributetablecolumn_node(self: DPYHTML5Translator, node: attributetablecolumn) -> None:
|
||||
self.body.append(self.starttag(node, 'div', CLASS='py-attribute-table-column'))
|
||||
|
||||
|
||||
def visit_attributetabletitle_node(self: DPYHTML5Translator, node: attributetabletitle) -> None:
|
||||
self.body.append(self.starttag(node, 'span'))
|
||||
|
||||
|
||||
def visit_attributetablebadge_node(self: DPYHTML5Translator, node: attributetablebadge) -> None:
|
||||
attributes = {
|
||||
'class': 'py-attribute-table-badge',
|
||||
'title': node['badge-type'],
|
||||
}
|
||||
self.body.append(self.starttag(node, 'span', **attributes))
|
||||
|
||||
|
||||
def visit_attributetable_item_node(self: DPYHTML5Translator, node: attributetable_item) -> None:
|
||||
self.body.append(self.starttag(node, 'li', CLASS='py-attribute-table-entry'))
|
||||
|
||||
|
||||
def depart_attributetable_node(self: DPYHTML5Translator, node: attributetable) -> None:
|
||||
self.body.append('</div>')
|
||||
|
||||
|
||||
def depart_attributetablecolumn_node(self: DPYHTML5Translator, node: attributetablecolumn) -> None:
|
||||
self.body.append('</div>')
|
||||
|
||||
|
||||
def depart_attributetabletitle_node(self: DPYHTML5Translator, node: attributetabletitle) -> None:
|
||||
self.body.append('</span>')
|
||||
|
||||
|
||||
def depart_attributetablebadge_node(self: DPYHTML5Translator, node: attributetablebadge) -> None:
|
||||
self.body.append('</span>')
|
||||
|
||||
|
||||
def depart_attributetable_item_node(self: DPYHTML5Translator, node: attributetable_item) -> None:
|
||||
self.body.append('</li>')
|
||||
|
||||
|
||||
_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: OptionSpec = {}
|
||||
|
||||
def parse_name(self, content: str) -> Tuple[str, str]:
|
||||
match = _name_parser_regex.match(content)
|
||||
if match is None:
|
||||
raise RuntimeError(f"content {content} somehow doesn't match regex in {self.env.docname}.")
|
||||
path, name = match.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(f'modulename somehow None for {content} in {self.env.docname}.')
|
||||
|
||||
return modulename, name
|
||||
|
||||
def run(self) -> List[attributetableplaceholder]:
|
||||
"""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>_('Methods')</span>
|
||||
<ul>
|
||||
<li>
|
||||
<a href="..."></a>
|
||||
<span class="py-attribute-badge" title="decorator">D</span>
|
||||
</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-doc'] = self.env.docname
|
||||
node['python-module'] = modulename
|
||||
node['python-class'] = name
|
||||
node['python-full-name'] = f'{modulename}.{name}'
|
||||
return [node]
|
||||
|
||||
|
||||
def build_lookup_table(env: BuildEnvironment) -> Dict[str, List[str]]:
|
||||
# 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, _, objtype, docname, _, _ in domain.get_objects():
|
||||
if objtype in ignored:
|
||||
continue
|
||||
|
||||
classname, _, child = fullname.rpartition('.')
|
||||
try:
|
||||
result[classname].append(child)
|
||||
except KeyError:
|
||||
result[classname] = [child]
|
||||
|
||||
return result
|
||||
|
||||
|
||||
class TableElement(NamedTuple):
|
||||
fullname: str
|
||||
label: str
|
||||
badge: Optional[attributetablebadge]
|
||||
|
||||
|
||||
def process_attributetable(app: Sphinx, doctree: nodes.Node, fromdocname: str) -> None:
|
||||
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, sorted(subitems, key=lambda c: c.label)))
|
||||
|
||||
table['python-class'] = fullname
|
||||
|
||||
if not table:
|
||||
node.replace_self([])
|
||||
else:
|
||||
node.replace_self([table])
|
||||
|
||||
|
||||
def get_class_results(
|
||||
lookup: Dict[str, List[str]], modulename: str, name: str, fullname: str
|
||||
) -> Dict[str, List[TableElement]]:
|
||||
module = importlib.import_module(modulename)
|
||||
cls = getattr(module, name)
|
||||
|
||||
groups: Dict[str, List[TableElement]] = {
|
||||
_('Attributes'): [],
|
||||
_('Methods'): [],
|
||||
}
|
||||
|
||||
try:
|
||||
members = lookup[fullname]
|
||||
except KeyError:
|
||||
return groups
|
||||
|
||||
for attr in members:
|
||||
attrlookup = f'{fullname}.{attr}'
|
||||
key = _('Attributes')
|
||||
badge = None
|
||||
label = attr
|
||||
value = None
|
||||
|
||||
for base in cls.__mro__:
|
||||
value = base.__dict__.get(attr)
|
||||
if value is not None:
|
||||
break
|
||||
|
||||
if value is not None:
|
||||
doc = value.__doc__ or ''
|
||||
if inspect.iscoroutinefunction(value) or doc.startswith('|coro|'):
|
||||
key = _('Methods')
|
||||
badge = attributetablebadge('async', 'async')
|
||||
badge['badge-type'] = _('coroutine')
|
||||
elif isinstance(value, classmethod):
|
||||
key = _('Methods')
|
||||
label = f'{name}.{attr}'
|
||||
badge = attributetablebadge('cls', 'cls')
|
||||
badge['badge-type'] = _('classmethod')
|
||||
elif inspect.isfunction(value):
|
||||
if doc.startswith(('A decorator', 'A shortcut decorator')):
|
||||
# finicky but surprisingly consistent
|
||||
key = _('Methods')
|
||||
badge = attributetablebadge('@', '@')
|
||||
badge['badge-type'] = _('decorator')
|
||||
elif inspect.isasyncgenfunction(value):
|
||||
key = _('Methods')
|
||||
badge = attributetablebadge('async for', 'async for')
|
||||
badge['badge-type'] = _('async iterable')
|
||||
else:
|
||||
key = _('Methods')
|
||||
badge = attributetablebadge('def', 'def')
|
||||
badge['badge-type'] = _('method')
|
||||
|
||||
groups[key].append(TableElement(fullname=attrlookup, label=label, badge=badge))
|
||||
|
||||
return groups
|
||||
|
||||
|
||||
def class_results_to_node(key: str, elements: Sequence[TableElement]) -> attributetablecolumn:
|
||||
title = attributetabletitle(key, key)
|
||||
ul = nodes.bullet_list('')
|
||||
for element in elements:
|
||||
ref = nodes.reference(
|
||||
'', '', internal=True, refuri=f'#{element.fullname}', anchorname='', *[nodes.Text(element.label)]
|
||||
)
|
||||
para = addnodes.compact_paragraph('', '', ref)
|
||||
if element.badge is not None:
|
||||
ul.append(attributetable_item('', element.badge, para))
|
||||
else:
|
||||
ul.append(attributetable_item('', para))
|
||||
|
||||
return attributetablecolumn('', title, ul)
|
||||
|
||||
|
||||
def setup(app: Sphinx) -> None:
|
||||
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(attributetablebadge, html=(visit_attributetablebadge_node, depart_attributetablebadge_node))
|
||||
app.add_node(attributetable_item, html=(visit_attributetable_item_node, depart_attributetable_item_node))
|
||||
app.add_node(attributetableplaceholder)
|
||||
app.connect('doctree-resolved', process_attributetable)
|
77
docs/extensions/builder.py
Normal file
77
docs/extensions/builder.py
Normal file
|
@ -0,0 +1,77 @@
|
|||
from sphinx.builders.html import StandaloneHTMLBuilder
|
||||
from sphinx.environment.adapters.indexentries import IndexEntries
|
||||
from sphinx.writers.html5 import HTML5Translator
|
||||
|
||||
class DPYHTML5Translator(HTML5Translator):
|
||||
def visit_section(self, node):
|
||||
self.section_level += 1
|
||||
self.body.append(
|
||||
self.starttag(node, 'section'))
|
||||
|
||||
def depart_section(self, node):
|
||||
self.section_level -= 1
|
||||
self.body.append('</section>\n')
|
||||
|
||||
def visit_table(self, node):
|
||||
self.body.append('<div class="table-wrapper">')
|
||||
super().visit_table(node)
|
||||
|
||||
def depart_table(self, node):
|
||||
super().depart_table(node)
|
||||
self.body.append('</div>')
|
||||
|
||||
class DPYStandaloneHTMLBuilder(StandaloneHTMLBuilder):
|
||||
# This is mostly copy pasted from Sphinx.
|
||||
def write_genindex(self) -> None:
|
||||
# the total count of lines for each index letter, used to distribute
|
||||
# the entries into two columns
|
||||
genindex = IndexEntries(self.env).create_index(self, group_entries=False)
|
||||
indexcounts = []
|
||||
for _k, entries in genindex:
|
||||
indexcounts.append(sum(1 + len(subitems)
|
||||
for _, (_, subitems, _) in entries))
|
||||
|
||||
genindexcontext = {
|
||||
'genindexentries': genindex,
|
||||
'genindexcounts': indexcounts,
|
||||
'split_index': self.config.html_split_index,
|
||||
}
|
||||
|
||||
if self.config.html_split_index:
|
||||
self.handle_page('genindex', genindexcontext,
|
||||
'genindex-split.html')
|
||||
self.handle_page('genindex-all', genindexcontext,
|
||||
'genindex.html')
|
||||
for (key, entries), count in zip(genindex, indexcounts):
|
||||
ctx = {'key': key, 'entries': entries, 'count': count,
|
||||
'genindexentries': genindex}
|
||||
self.handle_page('genindex-' + key, ctx,
|
||||
'genindex-single.html')
|
||||
else:
|
||||
self.handle_page('genindex', genindexcontext, 'genindex.html')
|
||||
|
||||
|
||||
def add_custom_jinja2(app):
|
||||
env = app.builder.templates.environment
|
||||
env.tests['prefixedwith'] = str.startswith
|
||||
env.tests['suffixedwith'] = str.endswith
|
||||
|
||||
def add_builders(app):
|
||||
"""This is necessary because RTD injects their own for some reason."""
|
||||
app.set_translator('html', DPYHTML5Translator, override=True)
|
||||
app.add_builder(DPYStandaloneHTMLBuilder, override=True)
|
||||
|
||||
try:
|
||||
original = app.registry.builders['readthedocs']
|
||||
except KeyError:
|
||||
pass
|
||||
else:
|
||||
injected_mro = tuple(base if base is not StandaloneHTMLBuilder else DPYStandaloneHTMLBuilder
|
||||
for base in original.mro()[1:])
|
||||
new_builder = type(original.__name__, injected_mro, {'name': 'readthedocs'})
|
||||
app.set_translator('readthedocs', DPYHTML5Translator, override=True)
|
||||
app.add_builder(new_builder, override=True)
|
||||
|
||||
def setup(app):
|
||||
add_builders(app)
|
||||
app.connect('builder-inited', add_custom_jinja2)
|
55
docs/extensions/details.py
Normal file
55
docs/extensions/details.py
Normal file
|
@ -0,0 +1,55 @@
|
|||
from docutils.parsers.rst import Directive
|
||||
from docutils.parsers.rst import states, directives
|
||||
from docutils.parsers.rst.roles import set_classes
|
||||
from docutils import nodes
|
||||
|
||||
class details(nodes.General, nodes.Element):
|
||||
pass
|
||||
|
||||
class summary(nodes.General, nodes.Element):
|
||||
pass
|
||||
|
||||
def visit_details_node(self, node):
|
||||
self.body.append(self.starttag(node, 'details', CLASS=node.attributes.get('class', '')))
|
||||
|
||||
def visit_summary_node(self, node):
|
||||
self.body.append(self.starttag(node, 'summary', CLASS=node.attributes.get('summary-class', '')))
|
||||
self.body.append(node.rawsource)
|
||||
|
||||
def depart_details_node(self, node):
|
||||
self.body.append('</details>\n')
|
||||
|
||||
def depart_summary_node(self, node):
|
||||
self.body.append('</summary>')
|
||||
|
||||
class DetailsDirective(Directive):
|
||||
final_argument_whitespace = True
|
||||
optional_arguments = 1
|
||||
|
||||
option_spec = {
|
||||
'class': directives.class_option,
|
||||
'summary-class': directives.class_option,
|
||||
}
|
||||
|
||||
has_content = True
|
||||
|
||||
def run(self):
|
||||
set_classes(self.options)
|
||||
self.assert_has_content()
|
||||
|
||||
text = '\n'.join(self.content)
|
||||
node = details(text, **self.options)
|
||||
|
||||
if self.arguments:
|
||||
summary_node = summary(self.arguments[0], **self.options)
|
||||
summary_node.source, summary_node.line = self.state_machine.get_source_and_line(self.lineno)
|
||||
node += summary_node
|
||||
|
||||
self.state.nested_parse(self.content, self.content_offset, node)
|
||||
return [node]
|
||||
|
||||
def setup(app):
|
||||
app.add_node(details, html=(visit_details_node, depart_details_node))
|
||||
app.add_node(summary, html=(visit_summary_node, depart_summary_node))
|
||||
app.add_directive('details', DetailsDirective)
|
||||
|
27
docs/extensions/exception_hierarchy.py
Normal file
27
docs/extensions/exception_hierarchy.py
Normal file
|
@ -0,0 +1,27 @@
|
|||
from docutils.parsers.rst import Directive
|
||||
from docutils.parsers.rst import states, directives
|
||||
from docutils.parsers.rst.roles import set_classes
|
||||
from docutils import nodes
|
||||
from sphinx.locale import _
|
||||
|
||||
class exception_hierarchy(nodes.General, nodes.Element):
|
||||
pass
|
||||
|
||||
def visit_exception_hierarchy_node(self, node):
|
||||
self.body.append(self.starttag(node, 'div', CLASS='exception-hierarchy-content'))
|
||||
|
||||
def depart_exception_hierarchy_node(self, node):
|
||||
self.body.append('</div>\n')
|
||||
|
||||
class ExceptionHierarchyDirective(Directive):
|
||||
has_content = True
|
||||
|
||||
def run(self):
|
||||
self.assert_has_content()
|
||||
node = exception_hierarchy('\n'.join(self.content))
|
||||
self.state.nested_parse(self.content, self.content_offset, node)
|
||||
return [node]
|
||||
|
||||
def setup(app):
|
||||
app.add_node(exception_hierarchy, html=(visit_exception_hierarchy_node, depart_exception_hierarchy_node))
|
||||
app.add_directive('exception_hierarchy', ExceptionHierarchyDirective)
|
22
docs/extensions/nitpick_file_ignorer.py
Normal file
22
docs/extensions/nitpick_file_ignorer.py
Normal file
|
@ -0,0 +1,22 @@
|
|||
import logging
|
||||
|
||||
from sphinx.application import Sphinx
|
||||
from sphinx.util import logging as sphinx_logging
|
||||
|
||||
|
||||
class NitpickFileIgnorer(logging.Filter):
|
||||
|
||||
def __init__(self, app: Sphinx) -> None:
|
||||
self.app = app
|
||||
super().__init__()
|
||||
|
||||
def filter(self, record: sphinx_logging.SphinxLogRecord) -> bool:
|
||||
if getattr(record, 'type', None) == 'ref':
|
||||
return record.location.get('refdoc') not in self.app.config.nitpick_ignore_files
|
||||
return True
|
||||
|
||||
|
||||
def setup(app: Sphinx):
|
||||
app.add_config_value('nitpick_ignore_files', [], '')
|
||||
f = NitpickFileIgnorer(app)
|
||||
sphinx_logging.getLogger('sphinx.transforms.post_transforms').logger.addFilter(f)
|
44
docs/extensions/resourcelinks.py
Normal file
44
docs/extensions/resourcelinks.py
Normal file
|
@ -0,0 +1,44 @@
|
|||
# Credit to sphinx.ext.extlinks for being a good starter
|
||||
# Copyright 2007-2020 by the Sphinx team
|
||||
# Licensed under BSD.
|
||||
|
||||
from typing import Any, Dict, List, Tuple
|
||||
|
||||
from docutils import nodes, utils
|
||||
from docutils.nodes import Node, system_message
|
||||
from docutils.parsers.rst.states import Inliner
|
||||
|
||||
import sphinx
|
||||
from sphinx.application import Sphinx
|
||||
from sphinx.util.nodes import split_explicit_title
|
||||
from sphinx.util.typing import RoleFunction
|
||||
|
||||
|
||||
def make_link_role(resource_links: Dict[str, str]) -> RoleFunction:
|
||||
def role(
|
||||
typ: str,
|
||||
rawtext: str,
|
||||
text: str,
|
||||
lineno: int,
|
||||
inliner: Inliner,
|
||||
options: Dict = {},
|
||||
content: List[str] = []
|
||||
) -> Tuple[List[Node], List[system_message]]:
|
||||
|
||||
text = utils.unescape(text)
|
||||
has_explicit_title, title, key = split_explicit_title(text)
|
||||
full_url = resource_links[key]
|
||||
if not has_explicit_title:
|
||||
title = full_url
|
||||
pnode = nodes.reference(title, title, internal=False, refuri=full_url)
|
||||
return [pnode], []
|
||||
return role
|
||||
|
||||
|
||||
def add_link_role(app: Sphinx) -> None:
|
||||
app.add_role('resource', make_link_role(app.config.resource_links))
|
||||
|
||||
def setup(app: Sphinx) -> Dict[str, Any]:
|
||||
app.add_config_value('resource_links', {}, 'env')
|
||||
app.connect('builder-inited', add_link_role)
|
||||
return {'version': sphinx.__display_version__, 'parallel_read_safe': True}
|
Loading…
Add table
Add a link
Reference in a new issue