diff --git a/docs/_static/style.css b/docs/_static/style.css index 85fbabe6d..d40a13a03 100644 --- a/docs/_static/style.css +++ b/docs/_static/style.css @@ -97,6 +97,13 @@ Historically however, thanks to: --rtd-ad-background: var(--grey-2); --rtd-ad-main-text: var(--grey-6); --rtd-ad-small-text: var(--grey-4); + --attribute-table-title: var(--grey-6); + --attribute-table-entry-border: var(--grey-3); + --attribute-table-entry-text: var(--grey-5); + --attribute-table-entry-hover-border: var(--blue); + --attribute-table-entry-hover-background: var(--grey-2); + --attribute-table-entry-hover-text: var(--blue); + --attribute-table-badge: var(--grey-7); } :root[data-font="sans"] { @@ -151,6 +158,13 @@ Historically however, thanks to: --rtd-ad-background: var(--grey-5); --rtd-ad-main-text: var(--grey-2); --rtd-ad-small-text: var(--grey-1); + --attribute-table-title: var(--grey-3); + --attribute-table-entry-border: var(--grey-5); + --attribute-table-entry-text: var(--grey-3); + --attribute-table-entry-hover-border: var(--blue); + --attribute-table-entry-hover-background: var(--grey-6); + --attribute-table-entry-hover-text: var(--blue); + --attribute-table-badge: var(--grey-4); } img[src$="snake_dark.svg"] { @@ -744,19 +758,64 @@ div.helpful > p.admonition-title::after { display: flex; flex-wrap: wrap; flex-direction: row; - justify-content: space-between; margin: 0 2em; padding-top: 16px; } +.py-attribute-table-column { + flex: 1 1 auto; +} + +.py-attribute-table-column:not(:first-child) { + margin-top: 1em; +} + .py-attribute-table-column > span { font-weight: bold; + color: var(--attribute-table-title); } main .py-attribute-table-column > ul { list-style: none; margin: 4px 0px; - padding-left: 12px; + padding-left: 0; + font-size: 0.95em; +} + +.py-attribute-table-entry { + margin: 0; + padding: 2px 0; + padding-left: 0.2em; + border-left: 2px solid var(--attribute-table-entry-border); + display: flex; + line-height: 1.2em; +} + +.py-attribute-table-entry > a { + padding-left: 0.5em; + color: var(--attribute-table-entry-text); + flex-grow: 1; +} + +.py-attribute-table-entry > a:hover { + color: var(--attribute-table-entry-hover-text); + text-decoration: none; +} + +.py-attribute-table-entry:hover { + background-color: var(--attribute-table-entry-hover-background); + border-left: 2px solid var(--attribute-table-entry-hover-border); + text-decoration: none; +} + +.py-attribute-table-badge { + flex-basis: 3em; + text-align: right; + font-size: 0.9em; + color: var(--attribute-table-badge); + -moz-user-select: none; + -webkit-user-select: none; + user-select: none; } pre { @@ -1076,6 +1135,10 @@ div.code-block-caption { font-size: 1.5em; } + .py-attribute-table-column:not(:first-child) { + margin-top: unset; + } + main img { display: block; margin-left: auto; diff --git a/docs/extensions/attributetable.py b/docs/extensions/attributetable.py index aa34ec95c..b173edaba 100644 --- a/docs/extensions/attributetable.py +++ b/docs/extensions/attributetable.py @@ -3,7 +3,7 @@ from sphinx.locale import _ from docutils import nodes from sphinx import addnodes -from collections import OrderedDict +from collections import OrderedDict, namedtuple import importlib import inspect import os @@ -21,6 +21,12 @@ class attributetabletitle(nodes.TextElement): 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, node): self.body.append('
' % node['python-class']) @@ -30,6 +36,16 @@ def visit_attributetablecolumn_node(self, node): def visit_attributetabletitle_node(self, node): self.body.append(self.starttag(node, 'span')) +def visit_attributetablebadge_node(self, node): + attributes = { + 'class': 'py-attribute-table-badge', + 'title': node['badge-type'], + } + self.body.append(self.starttag(node, 'span', **attributes)) + +def visit_attributetable_item_node(self, node): + self.body.append(self.starttag(node, 'li', CLASS='py-attribute-table-entry')) + def depart_attributetable_node(self, node): self.body.append('
') @@ -39,6 +55,12 @@ def depart_attributetablecolumn_node(self, node): def depart_attributetabletitle_node(self, node): self.body.append('') +def depart_attributetablebadge_node(self, node): + self.body.append('') + +def depart_attributetable_item_node(self, node): + self.body.append('') + _name_parser_regex = re.compile(r'(?P[\w.]+\.)?(?P\w+)') class PyAttributeTable(SphinxDirective): @@ -68,22 +90,20 @@ class PyAttributeTable(SphinxDirective):
_('Attributes') -
- - ... However, since this requires the tree to be complete @@ -120,6 +140,9 @@ def build_lookup_table(env): return result + +TableElement = namedtuple('TableElement', 'fullname label badge') + def process_attributetable(app, doctree, fromdocname): env = app.builder.env @@ -131,7 +154,7 @@ def process_attributetable(app, doctree, fromdocname): for label, subitems in groups.items(): if not subitems: continue - table.append(class_results_to_node(label, sorted(subitems))) + table.append(class_results_to_node(label, sorted(subitems, key=lambda c: c.label))) table['python-class'] = fullname @@ -145,11 +168,8 @@ def get_class_results(lookup, modulename, name, fullname): cls_dict = getattr(module, name).__dict__ groups = OrderedDict([ - ('Attributes', []), - ('Coroutines', []), - ('Classmethods', []), - ('Methods', []), - ('Decorators', []), + (_('Attributes'), []), + (_('Methods'), []), ]) try: @@ -159,38 +179,50 @@ def get_class_results(lookup, modulename, name, fullname): for attr in members: attrlookup = '%s.%s' % (fullname, attr) - key = 'Attributes' + key = _('Attributes') + badge = None 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' + key = _('Methods') + badge = attributetablebadge('async', 'async') + badge['badge-type'] = _('coroutine') elif isinstance(value, classmethod): - key = 'Classmethods' + key = _('Methods') + label = '%s.%s' % (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 = 'Decorators' + badge = attributetablebadge('@', '@') + badge['badge-type'] = _('decorator') + key = _('Methods') else: - key = 'Methods' + key = _('Methods') + badge = attributetablebadge('def', 'def') + badge['badge-type'] = _('method') - groups[key].append((attrlookup, label)) + groups[key].append(TableElement(fullname=attrlookup, label=label, badge=badge)) return groups def class_results_to_node(key, elements): title = attributetabletitle(key, key) ul = nodes.bullet_list('') - for fullname, label in elements: + for element in elements: ref = nodes.reference('', '', internal=True, - refuri='#' + fullname, + refuri='#' + element.fullname, anchorname='', - *[nodes.Text(label)]) + *[nodes.Text(element.label)]) para = addnodes.compact_paragraph('', '', ref) - item = nodes.list_item('', para) - ul.append(item) + if element.badge is not None: + ul.append(attributetable_item('', element.badge, para)) + else: + ul.append(attributetable_item('', para)) return attributetablecolumn('', title, ul) @@ -199,5 +231,7 @@ def setup(app): 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)