Skip to content

Commit fb6afac

Browse files
authored
Ensure reference renaming is parallel-safe (#136)
* Ensure reference renaming is parallel-safe This prefixes each reference with a token indicative of which docstring it belongs to and then relabels the text of the reference once the doctree is compiled * Fix missing argument * Install pdflatex in Travis * More TeX packages * Some sphinx require latexmk * Move imports * Fix comment * Warn about #130 * Fixes to relabelling of references: * Use label text not normalised text * Split at first - not last * Do not relabel non-docstring content * Fix logic for identifying non-docscring
1 parent 21a194e commit fb6afac

File tree

2 files changed

+56
-10
lines changed

2 files changed

+56
-10
lines changed

doc/format.rst

+6
Original file line numberDiff line numberDiff line change
@@ -387,6 +387,12 @@ The sections of a function's docstring are:
387387
should not be required to understand it. References are numbered, starting
388388
from one, in the order in which they are cited.
389389

390+
.. warning:: **References will break tables**
391+
392+
Where references like [1] appear in a tables within a numpydoc
393+
docstring, the table markup will be broken by numpydoc processing. See
394+
`numpydoc issue #130 <https://github.com/numpy/numpydoc/issues/130>`_
395+
390396
14. **Examples**
391397

392398
An optional section for examples, using the `doctest

numpydoc/numpydoc.py

+50-10
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,13 @@
2121
import sys
2222
import re
2323
import pydoc
24-
import sphinx
2524
import inspect
2625
import collections
26+
import hashlib
27+
28+
from docutils.nodes import citation, Text
29+
import sphinx
30+
from sphinx.addnodes import pending_xref, desc_content
2731

2832
if sphinx.__version__ < '1.0.1':
2933
raise RuntimeError("Sphinx 1.0.1 or newer is required")
@@ -37,9 +41,12 @@
3741
sixu = lambda s: unicode(s, 'unicode_escape')
3842

3943

40-
def rename_references(app, what, name, obj, options, lines,
41-
reference_offset=[0]):
42-
# replace reference numbers so that there are no duplicates
44+
HASH_LEN = 12
45+
46+
47+
def rename_references(app, what, name, obj, options, lines):
48+
# decorate reference numbers so that there are no duplicates
49+
# these are later undecorated in the doctree, in relabel_references
4350
references = set()
4451
for line in lines:
4552
line = line.strip()
@@ -49,19 +56,51 @@ def rename_references(app, what, name, obj, options, lines,
4956
references.add(m.group(1))
5057

5158
if references:
52-
for r in references:
53-
if r.isdigit():
54-
new_r = sixu("R%d") % (reference_offset[0] + int(r))
55-
else:
56-
new_r = sixu("%s%d") % (r, reference_offset[0])
59+
# we use a hash to mangle the reference name to avoid invalid names
60+
sha = hashlib.sha256()
61+
sha.update(name.encode('utf8'))
62+
prefix = 'R' + sha.hexdigest()[:HASH_LEN]
5763

64+
for r in references:
65+
new_r = prefix + '-' + r
5866
for i, line in enumerate(lines):
5967
lines[i] = lines[i].replace(sixu('[%s]_') % r,
6068
sixu('[%s]_') % new_r)
6169
lines[i] = lines[i].replace(sixu('.. [%s]') % r,
6270
sixu('.. [%s]') % new_r)
6371

64-
reference_offset[0] += len(references)
72+
73+
def _ascend(node, cls):
74+
while node and not isinstance(node, cls):
75+
node = node.parent
76+
return node
77+
78+
79+
def relabel_references(app, doc):
80+
# Change 'hash-ref' to 'ref' in label text
81+
for citation_node in doc.traverse(citation):
82+
if _ascend(citation_node, desc_content) is None:
83+
# no desc node in ancestry -> not in a docstring
84+
# XXX: should we also somehow check it's in a References section?
85+
continue
86+
label_node = citation_node[0]
87+
prefix, _, new_label = label_node[0].astext().partition('-')
88+
assert len(prefix) == HASH_LEN + 1
89+
new_text = Text(new_label)
90+
label_node.replace(label_node[0], new_text)
91+
92+
for id in citation_node['backrefs']:
93+
ref = doc.ids[id]
94+
ref_text = ref[0]
95+
96+
# Sphinx has created pending_xref nodes with [reftext] text.
97+
def matching_pending_xref(node):
98+
return (isinstance(node, pending_xref) and
99+
node[0].astext() == '[%s]' % ref_text)
100+
101+
for xref_node in ref.parent.traverse(matching_pending_xref):
102+
xref_node.replace(xref_node[0], Text('[%s]' % new_text))
103+
ref.replace(ref_text, new_text.copy())
65104

66105

67106
DEDUPLICATION_TAG = ' !! processed by numpydoc !!'
@@ -139,6 +178,7 @@ def setup(app, get_doc_object_=get_doc_object):
139178

140179
app.connect('autodoc-process-docstring', mangle_docstrings)
141180
app.connect('autodoc-process-signature', mangle_signature)
181+
app.connect('doctree-read', relabel_references)
142182
app.add_config_value('numpydoc_edit_link', None, False)
143183
app.add_config_value('numpydoc_use_plots', None, False)
144184
app.add_config_value('numpydoc_use_blockquotes', None, False)

0 commit comments

Comments
 (0)