2929 raise RuntimeError ("Sphinx 1.0.1 or newer is required" )
3030
3131from .docscrape_sphinx import get_doc_object , SphinxDocString
32- from sphinx .util .compat import Directive
3332
3433if sys .version_info [0 ] >= 3 :
3534 sixu = lambda s : s
@@ -139,7 +138,7 @@ def setup(app, get_doc_object_=get_doc_object):
139138 # Extra mangling domains
140139 app .add_domain (NumpyPythonDomain )
141140 app .add_domain (NumpyCDomain )
142-
141+
143142 metadata = {'parallel_read_safe' : True }
144143 return metadata
145144
@@ -190,6 +189,62 @@ class NumpyCDomain(ManglingDomainBase, CDomain):
190189 }
191190
192191
192+ def match_items (lines , content_old ):
193+ """Create items for mangled lines.
194+
195+ This function tries to match the lines in ``lines`` with the items (source
196+ file references and line numbers) in ``content_old``. The
197+ ``mangle_docstrings`` function changes the actual docstrings, but doesn't
198+ keep track of where each line came from. The manging does many operations
199+ on the original lines, which are hard to track afterwards.
200+
201+ Many of the line changes come from deleting or inserting blank lines. This
202+ function tries to match lines by ignoring blank lines. All other changes
203+ (such as inserting figures or changes in the references) are completely
204+ ignored, so the generated line numbers will be off if ``mangle_docstrings``
205+ does anything non-trivial.
206+
207+ This is a best-effort function and the real fix would be to make
208+ ``mangle_docstrings`` actually keep track of the ``items`` together with
209+ the ``lines``.
210+
211+ Examples
212+ --------
213+ >>> lines = ['', 'A', '', 'B', ' ', '', 'C', 'D']
214+ >>> lines_old = ['a', '', '', 'b', '', 'c']
215+ >>> items_old = [('file1.py', 0), ('file1.py', 1), ('file1.py', 2),
216+ ... ('file2.py', 0), ('file2.py', 1), ('file2.py', 2)]
217+ >>> content_old = ViewList(lines_old, items=items_old)
218+ >>> match_items(lines, content_old) # doctest: +NORMALIZE_WHITESPACE
219+ [('file1.py', 0), ('file1.py', 0), ('file2.py', 0), ('file2.py', 0),
220+ ('file2.py', 2), ('file2.py', 2), ('file2.py', 2), ('file2.py', 2)]
221+ >>> # first 2 ``lines`` are matched to 'a', second 2 to 'b', rest to 'c'
222+ >>> # actual content is completely ignored.
223+
224+ Notes
225+ -----
226+ The algorithm tries to match any line in ``lines`` with one in
227+ ``lines_old``. It skips over all empty lines in ``lines_old`` and assigns
228+ this line number to all lines in ``lines``, unless a non-empty line is
229+ found in ``lines`` in which case it goes to the next line in ``lines_old``.
230+
231+ """
232+ items_new = []
233+ lines_old = content_old .data
234+ items_old = content_old .items
235+ j = 0
236+ for i , line in enumerate (lines ):
237+ # go to next non-empty line in old:
238+ # line.strip() checks whether the string is all whitespace
239+ while j < len (lines_old ) - 1 and not lines_old [j ].strip ():
240+ j += 1
241+ items_new .append (items_old [j ])
242+ if line .strip () and j < len (lines_old ) - 1 :
243+ j += 1
244+ assert (len (items_new ) == len (lines ))
245+ return items_new
246+
247+
193248def wrap_mangling_directive (base_directive , objtype ):
194249 class directive (base_directive ):
195250 def run (self ):
@@ -205,7 +260,10 @@ def run(self):
205260
206261 lines = list (self .content )
207262 mangle_docstrings (env .app , objtype , name , None , None , lines )
208- self .content = ViewList (lines , self .content .parent )
263+ if self .content :
264+ items = match_items (lines , self .content )
265+ self .content = ViewList (lines , items = items ,
266+ parent = self .content .parent )
209267
210268 return base_directive .run (self )
211269
0 commit comments