From 5f42ba5ede87ef9550dcc4f4fc9d5326a1bb9ec4 Mon Sep 17 00:00:00 2001 From: "John P. McConnell" <78702323+johnpmcconnell@users.noreply.github.com> Date: Mon, 22 Feb 2021 19:03:22 -0500 Subject: [PATCH] Implementing meta field for specifying "self" reference in documentation for special methods --- README.rst | 20 ++ sphinxcontrib/prettyspecialmethods.py | 98 ++++++-- tests/test-autodoc-self-param/conf.py | 23 ++ tests/test-autodoc-self-param/index.rst | 4 + .../test_autodoc_self_param_module.py | 225 ++++++++++++++++++ tests/test-simple-self-param/conf.py | 4 + tests/test-simple-self-param/index.rst | 225 ++++++++++++++++++ tests/test_plaintext.py | 27 +++ 8 files changed, 603 insertions(+), 23 deletions(-) create mode 100644 tests/test-autodoc-self-param/conf.py create mode 100644 tests/test-autodoc-self-param/index.rst create mode 100644 tests/test-autodoc-self-param/test_autodoc_self_param_module.py create mode 100644 tests/test-simple-self-param/conf.py create mode 100644 tests/test-simple-self-param/index.rst diff --git a/README.rst b/README.rst index 9cdb099..2c45609 100644 --- a/README.rst +++ b/README.rst @@ -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 @@ -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 ----- diff --git a/sphinxcontrib/prettyspecialmethods.py b/sphinxcontrib/prettyspecialmethods.py index 4d5931e..77757b1 100644 --- a/sphinxcontrib/prettyspecialmethods.py +++ b/sphinxcontrib/prettyspecialmethods.py @@ -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( @@ -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, ]) ) @@ -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(' '), @@ -77,9 +78,9 @@ 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(']')), @@ -87,12 +88,12 @@ def brackets(parameters_node, selfref): 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(' '), @@ -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('<'), @@ -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'), @@ -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 ) @@ -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): @@ -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} diff --git a/tests/test-autodoc-self-param/conf.py b/tests/test-autodoc-self-param/conf.py new file mode 100644 index 0000000..ebcc94a --- /dev/null +++ b/tests/test-autodoc-self-param/conf.py @@ -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__', +} diff --git a/tests/test-autodoc-self-param/index.rst b/tests/test-autodoc-self-param/index.rst new file mode 100644 index 0000000..766ef9e --- /dev/null +++ b/tests/test-autodoc-self-param/index.rst @@ -0,0 +1,4 @@ +.. automodule:: test_autodoc_self_param_module + :members: + :special-members: + :undoc-members: diff --git a/tests/test-autodoc-self-param/test_autodoc_self_param_module.py b/tests/test-autodoc-self-param/test_autodoc_self_param_module.py new file mode 100644 index 0000000..3582440 --- /dev/null +++ b/tests/test-autodoc-self-param/test_autodoc_self_param_module.py @@ -0,0 +1,225 @@ +class MethodHolder: + # misc (alphabetical) + def __await__(self): + """:meta self-param: obj""" + pass + + def __call__(self, *args, **kwargs): + """:meta self-param: obj""" + pass + + def __dir__(self): + """:meta self-param: obj""" + pass + + def __format__(self, fmt): + """:meta self-param: obj""" + pass + + def __hash__(self): + """:meta self-param: obj""" + pass + + def __repr__(self): + """:meta self-param: obj""" + pass + + def __sizeof__(self): + """:meta self-param: obj""" + pass + + # type coercion + + def __str__(self): + """:meta self-param: obj""" + pass + + def __bytes__(self): + """:meta self-param: obj""" + pass + + def __bool__(self): + """:meta self-param: obj""" + pass + + def __int__(self): + """:meta self-param: obj""" + pass + + def __float__(self): + """:meta self-param: obj""" + pass + + def __complex__(self): + """:meta self-param: obj""" + pass + + def __index__(self): + """:meta self-param: obj""" + pass + + # attribute access + + def __getattr__(self, attr): + """:meta self-param: obj""" + pass + + def __setattr__(self, attr, value): + """:meta self-param: obj""" + pass + + def __delattr__(self, attr): + """:meta self-param: obj""" + pass + + # sequence methods + + def __contains__(self, value): + """:meta self-param: obj""" + pass + + def __getitem__(self, item): + """:meta self-param: obj""" + pass + + def __setitem__(self, item, value): + """:meta self-param: obj""" + pass + + def __delitem__(self, item): + """:meta self-param: obj""" + pass + + def __iter__(self): + """:meta self-param: obj""" + pass + + def __len__(self): + """:meta self-param: obj""" + pass + + def __length_hint__(self): + """:meta self-param: obj""" + pass + + def __reversed__(self): + """:meta self-param: obj""" + pass + + # unary operators (alphabetical) + + def __invert__(self): + """:meta self-param: obj""" + pass + + def __neg__(self): + """:meta self-param: obj""" + pass + + def __pos__(self): + """:meta self-param: obj""" + pass + + # binary operators (alphabetical) + + def __add__(self, other): + """:meta self-param: obj""" + pass + + def __and__(self, other): + """:meta self-param: obj""" + pass + + def __divmod__(self, other): + """:meta self-param: obj""" + pass + + def __eq__(self, other): + """:meta self-param: obj""" + pass + + def __floordiv__(self, other): + """:meta self-param: obj""" + pass + + def __ge__(self, other): + """:meta self-param: obj""" + pass + + def __gt__(self, other): + """:meta self-param: obj""" + pass + + def __le__(self, other): + """:meta self-param: obj""" + pass + + def __lshift__(self, other): + """:meta self-param: obj""" + pass + + def __lt__(self, other): + """:meta self-param: obj""" + pass + + def __matmul__(self, other): + """:meta self-param: obj""" + pass + + def __mod__(self, other): + """:meta self-param: obj""" + pass + + def __mul__(self, other): + """:meta self-param: obj""" + pass + + def __ne__(self, other): + """:meta self-param: obj""" + pass + + def __or__(self, other): + """:meta self-param: obj""" + pass + + def __pow__(self, other): + """:meta self-param: obj""" + pass + + def __rshift__(self, other): + """:meta self-param: obj""" + pass + + def __sub__(self, other): + """:meta self-param: obj""" + pass + + def __truediv__(self, other): + """:meta self-param: obj""" + pass + + def __xor__(self, other): + """:meta self-param: obj""" + pass + + # other math + + def __abs__(self): + """:meta self-param: obj""" + pass + + def __ceil__(self): + """:meta self-param: obj""" + pass + + def __floor__(self): + """:meta self-param: obj""" + pass + + def __round__(self, n): + """:meta self-param: obj""" + pass + + def __trunc__(self): + """:meta self-param: obj""" + pass diff --git a/tests/test-simple-self-param/conf.py b/tests/test-simple-self-param/conf.py new file mode 100644 index 0000000..f94cbad --- /dev/null +++ b/tests/test-simple-self-param/conf.py @@ -0,0 +1,4 @@ +extensions = ['sphinxcontrib.prettyspecialmethods'] + +# The suffix of source filenames. +source_suffix = '.rst' diff --git a/tests/test-simple-self-param/index.rst b/tests/test-simple-self-param/index.rst new file mode 100644 index 0000000..00a59c9 --- /dev/null +++ b/tests/test-simple-self-param/index.rst @@ -0,0 +1,225 @@ +# misc (alphabetical) + +.. py:method:: __await__() + + :meta self-param: obj + +.. py:method:: __call__(*args, **kwargs) + + :meta self-param: obj + +.. py:method:: __dir__() + + :meta self-param: obj + +.. py:method:: __format__(fmt) + + :meta self-param: obj + +.. py:method:: __hash__() + + :meta self-param: obj + +.. py:method:: __repr__() + + :meta self-param: obj + +.. py:method:: __sizeof__() + + :meta self-param: obj + +# type coercion + +.. py:method:: __str__() + + :meta self-param: obj + +.. py:method:: __bytes__() + + :meta self-param: obj + +.. py:method:: __bool__() + + :meta self-param: obj + +.. py:method:: __int__() + + :meta self-param: obj + +.. py:method:: __float__() + + :meta self-param: obj + +.. py:method:: __complex__() + + :meta self-param: obj + +.. py:method:: __index__() + + :meta self-param: obj + +# attribute access + +.. py:method:: __getattr__(attr) + + :meta self-param: obj + +.. py:method:: __setattr__(attr, value) + + :meta self-param: obj + +.. py:method:: __delattr__(attr) + + :meta self-param: obj + +# sequence methods + +.. py:method:: __contains__(value) + + :meta self-param: obj + +.. py:method:: __getitem__(item) + + :meta self-param: obj + +.. py:method:: __setitem__(item, value) + + :meta self-param: obj + +.. py:method:: __delitem__(item) + + :meta self-param: obj + +.. py:method:: __iter__() + + :meta self-param: obj + +.. py:method:: __len__() + + :meta self-param: obj + +.. py:method:: __length_hint__() + + :meta self-param: obj + +.. py:method:: __reversed__() + + :meta self-param: obj + +# unary operators (alphabetical) + +.. py:method:: __invert__() + + :meta self-param: obj + +.. py:method:: __neg__() + + :meta self-param: obj + +.. py:method:: __pos__() + + :meta self-param: obj + +# binary operators (alphabetical) + +.. py:method:: __add__(other) + + :meta self-param: obj + +.. py:method:: __and__(other) + + :meta self-param: obj + +.. py:method:: __divmod__(other) + + :meta self-param: obj + +.. py:method:: __eq__(other) + + :meta self-param: obj + +.. py:method:: __floordiv__(other) + + :meta self-param: obj + +.. py:method:: __ge__(other) + + :meta self-param: obj + +.. py:method:: __gt__(other) + + :meta self-param: obj + +.. py:method:: __le__(other) + + :meta self-param: obj + +.. py:method:: __lshift__(other) + + :meta self-param: obj + +.. py:method:: __lt__(other) + + :meta self-param: obj + +.. py:method:: __matmul__(other) + + :meta self-param: obj + +.. py:method:: __mod__(other) + + :meta self-param: obj + +.. py:method:: __mul__(other) + + :meta self-param: obj + +.. py:method:: __ne__(other) + + :meta self-param: obj + +.. py:method:: __or__(other) + + :meta self-param: obj + +.. py:method:: __pow__(other) + + :meta self-param: obj + +.. py:method:: __rshift__(other) + + :meta self-param: obj + +.. py:method:: __sub__(other) + + :meta self-param: obj + +.. py:method:: __truediv__(other) + + :meta self-param: obj + +.. py:method:: __xor__(other) + + :meta self-param: obj + +# other math + +.. py:method:: __abs__() + + :meta self-param: obj + +.. py:method:: __ceil__() + + :meta self-param: obj + +.. py:method:: __floor__() + + :meta self-param: obj + +.. py:method:: __round__(n) + + :meta self-param: obj + +.. py:method:: __trunc__() + + :meta self-param: obj diff --git a/tests/test_plaintext.py b/tests/test_plaintext.py index 325e326..aac9efb 100644 --- a/tests/test_plaintext.py +++ b/tests/test_plaintext.py @@ -83,6 +83,16 @@ def test_domain_py_objects(app, status, warning): assert lines == SIMPLE_RESULT +@pytest.mark.sphinx(testroot='simple-self-param', buildername='text') +def test_domain_py_objects_with_self_param(app, status, warning): + app.builder.build_all() + result = (app.outdir / 'index.txt').read_text() + + lines = [line.rstrip('\n') for line in result.splitlines() if line.strip()] + + assert lines == [line.replace('self', 'obj') for line in SIMPLE_RESULT] + + @pytest.mark.sphinx(testroot='autodoc', buildername='text') def test_autodoc_module(app, status, warning): app.builder.build_all() @@ -94,3 +104,20 @@ def test_autodoc_module(app, status, warning): expected.insert(0, 'class test_autodoc_module.MethodHolder') assert lines == expected + + +@pytest.mark.sphinx(testroot='autodoc-self-param', buildername='text') +def test_autodoc_module_with_self_param(app, status, warning): + app.builder.build_all() + result = (app.outdir / 'index.txt').read_text() + + lines = [line.strip() for line in result.splitlines() if line.strip()] + + expected = [ + line.replace('self', 'obj') + for line in SIMPLE_RESULT + if not line.startswith('#') + ] + expected.insert(0, 'class test_autodoc_self_param_module.MethodHolder') + + assert lines == expected