Skip to content

Commit 9cf9ce0

Browse files
authored
doc_stack/special_methods (#1855)
* initial commit for doc_stack/special_methods * Add sphinxcontrib.prettyspecialmethods * Switch to fork of prettyspecialmethods * fix * fix
1 parent 3ee2ddc commit 9cf9ce0

File tree

2 files changed

+299
-0
lines changed

2 files changed

+299
-0
lines changed

doc/conf.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
'show-inheritance': True
3737
}
3838
toc_object_entries_show_parents = 'hide'
39+
prettyspecialmethods_signature_prefix = '🧙'
3940

4041
sys.path.insert(0, os.path.abspath('..'))
4142
sys.path.insert(0, os.path.abspath('../arcade'))
@@ -64,6 +65,7 @@
6465
'sphinx.ext.viewcode',
6566
'sphinx_copybutton',
6667
'sphinx_sitemap',
68+
'doc.extensions.prettyspecialmethods'
6769
]
6870

6971
# --- Spell check. Never worked well.
Lines changed: 297 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,297 @@
1+
# This is a fork of https://github.com/sphinx-contrib/prettyspecialmethods / https://pypi.org/project/sphinxcontrib-prettyspecialmethods/
2+
# Specifically, it is based off the modified version linked from this issue:
3+
# https://github.com/sphinx-contrib/prettyspecialmethods/issues/16
4+
5+
"""
6+
sphinxcontrib.prettyspecialmethods
7+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
8+
9+
Shows special methods as the python syntax that invokes them
10+
11+
:copyright: Copyright 2018 by Thomas Smith
12+
:license: MIT, see LICENSE for details.
13+
"""
14+
15+
# import pbr.version
16+
import sphinx.addnodes as SphinxNodes
17+
from docutils import nodes
18+
from docutils.nodes import Text
19+
from typing import TYPE_CHECKING, List, Tuple
20+
import sphinx.domains.python
21+
from sphinx.domains.python import py_sig_re
22+
from docutils.parsers.rst import directives
23+
import sphinx.ext.autodoc
24+
25+
26+
# __version__ = pbr.version.VersionInfo(
27+
# 'prettyspecialmethods').version_string()
28+
29+
30+
ATTR_TOC_SIG = '_prettyspecialmethods_sig'
31+
CONF_SIG_PREFIX = 'prettyspecialmethods_signature_prefix'
32+
33+
34+
def self_identifier(name = 'self'):
35+
# Format like a parameter, though we cannot use desc_parameter because it
36+
# causes error in html emitter
37+
return SphinxNodes.desc_sig_name('', name)
38+
39+
40+
# Parameter nodes cannot be used outside of a parameterlist, because it breaks
41+
# the html emitter. So we unpack their contents.
42+
# Better than astext() because it preserves references.
43+
def unwrap_parameter(parameters_node, index = 0):
44+
if len(parameters_node.children) > index:
45+
return parameters_node.children[index].children
46+
return ()
47+
48+
49+
# For keywords/builtins and punctuation that describes the operator and should
50+
# be formatted similar to a method or attribute's name.
51+
# For example, in the case of `del self[index]`: `del`, `[` and `]` should be
52+
# formatted like the method name `__delitem__` would have been formatted.
53+
def operator_name(str):
54+
return SphinxNodes.desc_name('', '', Text(str))
55+
def operator_punctuation(str):
56+
return SphinxNodes.desc_name('', '', Text(str))
57+
58+
59+
# For punctuation that is not part of the operator
60+
def punctuation(str):
61+
return SphinxNodes.desc_sig_punctuation('', str)
62+
63+
64+
def patch_node(node, text=None, children=None, *, constructor=None):
65+
if constructor is None:
66+
constructor = node.__class__
67+
68+
if text is None:
69+
text = node.text
70+
71+
if children is None:
72+
children = node.children
73+
74+
return constructor(
75+
node.source,
76+
text,
77+
*children,
78+
**node.attributes,
79+
)
80+
81+
82+
def function_transformer(new_name):
83+
def xf(name_node, parameters_node):
84+
return (
85+
patch_node(name_node, new_name, ()),
86+
patch_node(parameters_node, '', [
87+
self_identifier(),
88+
*parameters_node.children,
89+
])
90+
)
91+
92+
return xf
93+
94+
95+
def unary_op_transformer(op):
96+
def xf(name_node, parameters_node):
97+
return (
98+
patch_node(name_node, op, ()),
99+
self_identifier(),
100+
)
101+
102+
return xf
103+
104+
105+
def binary_op_transformer(op):
106+
def xf(name_node, parameters_node):
107+
return (
108+
self_identifier(),
109+
Text(' '),
110+
patch_node(name_node, op, ()),
111+
Text(' '),
112+
*unwrap_parameter(parameters_node)
113+
)
114+
115+
return xf
116+
117+
118+
def brackets(parameters_node):
119+
return [
120+
self_identifier(),
121+
operator_punctuation('['),
122+
*unwrap_parameter(parameters_node),
123+
operator_punctuation(']')
124+
]
125+
126+
127+
SPECIAL_METHODS = {
128+
'__getitem__': lambda name_node, parameters_node: (
129+
brackets(parameters_node)
130+
),
131+
'__setitem__': lambda name_node, parameters_node: (
132+
*brackets(parameters_node),
133+
Text(' '),
134+
operator_punctuation('='),
135+
Text(' '),
136+
*unwrap_parameter(parameters_node, 1)
137+
),
138+
'__delitem__': lambda name_node, parameters_node: (
139+
SphinxNodes.desc_name('', '', Text('del')),
140+
Text(' '),
141+
*brackets(parameters_node),
142+
),
143+
'__contains__': lambda name_node, parameters_node: (
144+
*unwrap_parameter(parameters_node),
145+
Text(' '),
146+
SphinxNodes.desc_name('', '', Text('in')),
147+
Text(' '),
148+
self_identifier(),
149+
),
150+
151+
'__await__': lambda name_node, parameters_node: (
152+
SphinxNodes.desc_name('', '', Text('await')),
153+
Text(' '),
154+
self_identifier(),
155+
),
156+
157+
'__lt__': binary_op_transformer('<'),
158+
'__le__': binary_op_transformer('<='),
159+
'__eq__': binary_op_transformer('=='),
160+
'__ne__': binary_op_transformer('!='),
161+
'__gt__': binary_op_transformer('>'),
162+
'__ge__': binary_op_transformer('>='),
163+
164+
'__hash__': function_transformer('hash'),
165+
'__len__': function_transformer('len'),
166+
'__iter__': function_transformer('iter'),
167+
'__str__': function_transformer('str'),
168+
'__repr__': function_transformer('repr'),
169+
170+
'__add__': binary_op_transformer('+'),
171+
'__sub__': binary_op_transformer('-'),
172+
'__mul__': binary_op_transformer('*'),
173+
'__matmul__': binary_op_transformer('@'),
174+
'__truediv__': binary_op_transformer('/'),
175+
'__floordiv__': binary_op_transformer('//'),
176+
'__mod__': binary_op_transformer('%'),
177+
'__divmod__': function_transformer('divmod'),
178+
'__pow__': binary_op_transformer('**'),
179+
'__lshift__': binary_op_transformer('<<'),
180+
'__rshift__': binary_op_transformer('>>'),
181+
'__and__': binary_op_transformer('&'),
182+
'__xor__': binary_op_transformer('^'),
183+
'__or__': binary_op_transformer('|'),
184+
185+
'__neg__': unary_op_transformer('-'),
186+
'__pos__': unary_op_transformer('+'),
187+
'__abs__': function_transformer('abs'),
188+
'__invert__': unary_op_transformer('~'),
189+
190+
'__call__': lambda name_node, parameters_node: (
191+
self_identifier(),
192+
patch_node(parameters_node, '', parameters_node.children)
193+
),
194+
'__getattr__': function_transformer('getattr'),
195+
'__setattr__': function_transformer('setattr'),
196+
'__delattr__': function_transformer('delattr'),
197+
198+
'__bool__': function_transformer('bool'),
199+
'__int__': function_transformer('int'),
200+
'__float__': function_transformer('float'),
201+
'__complex__': function_transformer('complex'),
202+
'__bytes__': function_transformer('bytes'),
203+
204+
# could show this as "{:...}".format(self) if we wanted
205+
'__format__': function_transformer('format'),
206+
207+
'__index__': function_transformer('operator.index'),
208+
'__length_hint__': function_transformer('operator.length_hint'),
209+
'__ceil__': function_transformer('math.ceil'),
210+
'__floor__': function_transformer('math.floor'),
211+
'__trunc__': function_transformer('math.trunc'),
212+
'__round__': function_transformer('round'),
213+
214+
'__sizeof__': function_transformer('sys.getsizeof'),
215+
'__dir__': function_transformer('dir'),
216+
'__reversed__': function_transformer('reversed'),
217+
}
218+
219+
220+
class PyMethod(sphinx.domains.python.PyMethod):
221+
"""
222+
Replacement for sphinx's built-in PyMethod directive, behaves identically
223+
except for tweaks to special methods.
224+
"""
225+
226+
option_spec = sphinx.domains.python.PyMethod.option_spec.copy()
227+
option_spec.update({
228+
'selfparam': directives.unchanged_required,
229+
})
230+
231+
def _get_special_method(self, sig: str):
232+
"If this is a special method, return its name, otherwise None"
233+
m = py_sig_re.match(sig)
234+
if m is None:
235+
return None
236+
prefix, method_name, typeparamlist, arglist, retann = m.groups()
237+
if method_name not in SPECIAL_METHODS:
238+
return None
239+
return method_name
240+
241+
def get_signature_prefix(self, sig: str) -> List[nodes.Node]:
242+
prefix = super().get_signature_prefix(sig)
243+
signature_prefix = getattr(self.env.app.config, CONF_SIG_PREFIX)
244+
if signature_prefix:
245+
method_name = self._get_special_method(sig)
246+
if method_name is not None:
247+
prefix.append(nodes.Text(signature_prefix))
248+
prefix.append(SphinxNodes.desc_sig_space())
249+
return prefix
250+
251+
def handle_signature(self, sig: str, signode: SphinxNodes.desc_signature) -> Tuple[str, str]:
252+
method_name = self._get_special_method(sig)
253+
254+
result = super().handle_signature(sig, signode)
255+
256+
if method_name is None:
257+
return result
258+
259+
sig_rewriter = SPECIAL_METHODS[method_name]
260+
name_node = signode.next_node(SphinxNodes.desc_name)
261+
parameters_node = signode.next_node(SphinxNodes.desc_parameterlist)
262+
263+
replacement = sig_rewriter(name_node, parameters_node)
264+
parent = name_node.parent
265+
parent.insert(parent.index(name_node), replacement)
266+
parameters_node.replace_self(())
267+
parent.remove(name_node)
268+
269+
# Generate TOC name by doing the same rewrite w/ typehints stripped from
270+
# params, then converting to str
271+
parameters_sans_types = [SphinxNodes.desc_parameter('', '', p.children[0]) for p in parameters_node.children]
272+
parameters_node_sans_types = SphinxNodes.desc_parameterlist('', '',
273+
*parameters_sans_types
274+
)
275+
replacement_w_out_types = sig_rewriter(name_node, parameters_node_sans_types)
276+
toc_name = ''.join([node.astext() for node in replacement_w_out_types])
277+
signode.attributes[ATTR_TOC_SIG] = toc_name
278+
279+
return result
280+
281+
def _toc_entry_name(self, sig_node: SphinxNodes.desc_signature):
282+
# TODO support toc_object_entries_show_parents?
283+
# How would that work? Show class name in place of `self`?
284+
return sig_node.attributes.get(ATTR_TOC_SIG, super()._toc_entry_name(sig_node))
285+
286+
287+
def show_special_methods(app, what, name, obj, skip, options):
288+
if what == 'class' and name in SPECIAL_METHODS and getattr(obj, '__doc__', None):
289+
return False
290+
291+
292+
def setup(app):
293+
app.add_config_value(CONF_SIG_PREFIX, 'implements', True)
294+
app.connect('autodoc-skip-member', show_special_methods)
295+
app.registry.add_directive_to_domain('py', 'method', PyMethod)
296+
# return {'version': __version__, 'parallel_read_safe': True}
297+
return {'parallel_read_safe': True}

0 commit comments

Comments
 (0)