Skip to content

Commit

Permalink
Implementing meta field for specifying "self" reference in documentat…
Browse files Browse the repository at this point in the history
…ion for special methods
  • Loading branch information
johnpmcconnell committed Feb 27, 2021
1 parent ef973ef commit 5f42ba5
Show file tree
Hide file tree
Showing 8 changed files with 603 additions and 23 deletions.
20 changes: 20 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ as
self + other
Docstring

It also works when using autodoc on a class that implements these methods.

After installing this module, add the following to your `conf.py` to enable it

Expand All @@ -38,6 +39,25 @@ After installing this module, add the following to your `conf.py` to enable it
'sphinxcontrib.prettyspecialmethods',
]
Changing "self"
---------------

If a `meta info field`_ named :code:`self-param` is included in the docstring, its
value will be used in place of "self" in the output:

.. _meta info field: https://www.sphinx-doc.org/en/master/usage/restructuredtext/domains.html#info-field-lists

.. code-block:: rst
.. method:: __add__(other)
Docstring
:meta self-param: obj
renders to

obj + other
Docstring


Links
-----
Expand Down
98 changes: 75 additions & 23 deletions sphinxcontrib/prettyspecialmethods.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,13 @@

import pbr.version
import sphinx.addnodes as SphinxNodes
from docutils.nodes import Text, emphasis, inline
from sphinx.transforms import SphinxTransform
from docutils.nodes import Text, emphasis, field, field_name, field_body, inline, pending

if False:
# For type annotations
from typing import Any, Dict # noqa
from docutils.nodes import Node # noqa
from sphinx.application import Sphinx # noqa

__version__ = pbr.version.VersionInfo(
Expand All @@ -41,11 +42,11 @@ def patch_node(node, text=None, children=None, *, constructor=None):


def function_transformer(new_name):
def xf(name_node, parameters_node, selfref):
def xf(name_node, parameters_node, self_param):
return (
patch_node(name_node, new_name, ()),
patch_node(parameters_node, '', [
SphinxNodes.desc_parameter('', selfref),
SphinxNodes.desc_parameter('', self_param),
*parameters_node.children,
])
)
Expand All @@ -54,20 +55,20 @@ def xf(name_node, parameters_node, selfref):


def unary_op_transformer(op):
def xf(name_node, parameters_node, selfref):
def xf(name_node, parameters_node, self_param):
return (
patch_node(name_node, op, ()),
emphasis('', selfref),
emphasis('', self_param),
)

return xf


def binary_op_transformer(op):
def xf(name_node, parameters_node, selfref):
def xf(name_node, parameters_node, self_param):
return inline(
'', '',
emphasis('', selfref),
emphasis('', self_param),
Text(' '),
patch_node(name_node, op, ()),
Text(' '),
Expand All @@ -77,22 +78,22 @@ def xf(name_node, parameters_node, selfref):
return xf


def brackets(parameters_node, selfref):
def brackets(parameters_node, self_param):
return [
emphasis('', selfref),
emphasis('', self_param),
SphinxNodes.desc_name('', '', Text('[')),
emphasis('', parameters_node.children[0].astext()),
SphinxNodes.desc_name('', '', Text(']')),
]


SPECIAL_METHODS = {
'__getitem__': lambda name_node, parameters_node, selfref: inline(
'', '', *brackets(parameters_node, selfref)
'__getitem__': lambda name_node, parameters_node, self_param: inline(
'', '', *brackets(parameters_node, self_param)
),
'__setitem__': lambda name_node, parameters_node, selfref: inline(
'__setitem__': lambda name_node, parameters_node, self_param: inline(
'', '',
*brackets(parameters_node, selfref),
*brackets(parameters_node, self_param),
Text(' '),
SphinxNodes.desc_name('', '', Text('=')),
Text(' '),
Expand All @@ -101,26 +102,26 @@ def brackets(parameters_node, selfref):
if len(parameters_node.children) > 1 else ''
)),
),
'__delitem__': lambda name_node, parameters_node, selfref: inline(
'__delitem__': lambda name_node, parameters_node, self_param: inline(
'', '',
SphinxNodes.desc_name('', '', Text('del')),
Text(' '),
*brackets(parameters_node, selfref),
*brackets(parameters_node, self_param),
),
'__contains__': lambda name_node, parameters_node, selfref: inline(
'__contains__': lambda name_node, parameters_node, self_param: inline(
'', '',
emphasis('', parameters_node.children[0].astext()),
Text(' '),
SphinxNodes.desc_name('', '', Text('in')),
Text(' '),
emphasis('', selfref),
emphasis('', self_param),
),

'__await__': lambda name_node, parameters_node, selfref: inline(
'__await__': lambda name_node, parameters_node, self_param: inline(
'', '',
SphinxNodes.desc_name('', '', Text('await')),
Text(' '),
emphasis('', selfref),
emphasis('', self_param),
),

'__lt__': binary_op_transformer('<'),
Expand Down Expand Up @@ -156,8 +157,8 @@ def brackets(parameters_node, selfref):
'__abs__': function_transformer('abs'),
'__invert__': unary_op_transformer('~'),

'__call__': lambda name_node, parameters_node, selfref: (
emphasis('', selfref),
'__call__': lambda name_node, parameters_node, self_param: (
emphasis('', self_param),
patch_node(parameters_node, '', parameters_node.children)
),
'__getattr__': function_transformer('getattr'),
Expand Down Expand Up @@ -186,12 +187,51 @@ def brackets(parameters_node, selfref):
}


class PendingSelfParamName(pending):
def __init__(self, name):
# type(str)
super().__init__(
transform=PrettifySpecialMethods,
details={'self_param': name},
)

@property
def name(self):
# type() -> str
return self.details['self_param']


def is_meta_self_param_info_field(node):
# type: (Node)
if not isinstance(node, field):
return False

name = node.next_node(field_name).astext()
return name == 'meta self-param'


def convert_meta_self_param(app, domain, objtype, contentnode):
# type: (Sphinx, str, str, Node) -> Dict[str, Any]
if not domain == 'py' or 'method' not in objtype:
return

# Note: Using next_node means we only find the first instance
# of selfparam. Additional selfparam fields are ignored and eventually
# deleted by the Python domain's meta filter.
selfparam_field = contentnode.next_node(is_meta_self_param_info_field)

if selfparam_field:
selfparam: str = selfparam_field.next_node(field_body).astext()
contentnode.append(PendingSelfParamName(selfparam))
selfparam_field.replace_self(())


class PrettifySpecialMethods(SphinxTransform):
default_priority = 800

def apply(self):
methods = (
sig for sig in self.document.traverse(SphinxNodes.desc_signature)
sig.parent for sig in self.document.traverse(SphinxNodes.desc_signature)
if 'class' in sig
)

Expand All @@ -200,11 +240,22 @@ def apply(self):
method_name = name_node.astext()

if method_name in SPECIAL_METHODS:
# Determine name to use for self in new specification
# using first child occurence
pending_self_param = ref.next_node(PendingSelfParamName)
self_param = pending_self_param.name if pending_self_param else 'self'

parameters_node = ref.next_node(SphinxNodes.desc_parameterlist)

name_node.replace_self(SPECIAL_METHODS[method_name](name_node, parameters_node, 'self'))
new_sig = SPECIAL_METHODS[method_name](name_node, parameters_node, self_param)

name_node.replace_self(new_sig)
parameters_node.replace_self(())

# Remove ALL occurrences of PendingSelfParamName
for p in self.document.traverse(PendingSelfParamName):
p.replace_self(())


def show_special_methods(app, what, name, obj, skip, options):
if what == 'class' and name in SPECIAL_METHODS and getattr(obj, '__doc__', None):
Expand All @@ -214,6 +265,7 @@ def show_special_methods(app, what, name, obj, skip, options):
def setup(app):
# type: (Sphinx) -> Dict[str, Any]
app.add_transform(PrettifySpecialMethods)
app.connect('object-description-transform', convert_meta_self_param, priority=450)
app.setup_extension('sphinx.ext.autodoc')
app.connect('autodoc-skip-member', show_special_methods)
return {'version': __version__, 'parallel_read_safe': True}
23 changes: 23 additions & 0 deletions tests/test-autodoc-self-param/conf.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import sys
import os


doc_module_path = os.path.dirname(__file__)
if doc_module_path not in sys.path:
sys.path.append(doc_module_path)


extensions = [
'sphinx.ext.autodoc',
'sphinxcontrib.prettyspecialmethods',
]


# The suffix of source filenames.
source_suffix = '.rst'


autodoc_default_options = {
'member-order': 'bysource',
'exclude-members': '__weakref__,__dict__,__module__',
}
4 changes: 4 additions & 0 deletions tests/test-autodoc-self-param/index.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
.. automodule:: test_autodoc_self_param_module
:members:
:special-members:
:undoc-members:
Loading

0 comments on commit 5f42ba5

Please sign in to comment.