Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.9/dist-packages/numpydoc/numpydoc.py: 16%
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1"""
2========
3numpydoc
4========
6Sphinx extension that handles docstrings in the Numpy standard format. [1]
8It will:
10- Convert Parameters etc. sections to field lists.
11- Convert See Also section to a See also entry.
12- Renumber references.
13- Extract the signature from the docstring, if it can't be determined
14 otherwise.
16.. [1] https://github.com/numpy/numpydoc
18"""
20import hashlib
21import inspect
22import itertools
23import pydoc
24import re
25from collections.abc import Callable
26from copy import deepcopy
28from docutils.nodes import Text, citation, comment, inline, reference, section
29from sphinx.addnodes import desc_content, pending_xref
30from sphinx.util import logging
32from . import __version__
33from .docscrape_sphinx import get_doc_object
34from .validate import get_validation_checks, validate
35from .xref import DEFAULT_LINKS
37logger = logging.getLogger(__name__)
39HASH_LEN = 12
42def _traverse_or_findall(node, condition, **kwargs):
43 """Triage node.traverse (docutils <0.18.1) vs node.findall.
45 TODO: This check can be removed when the minimum supported docutils version
46 for numpydoc is docutils>=0.18.1
47 """
48 return (
49 node.findall(condition, **kwargs)
50 if hasattr(node, "findall")
51 else node.traverse(condition, **kwargs)
52 )
55def rename_references(app, what, name, obj, options, lines):
56 # decorate reference numbers so that there are no duplicates
57 # these are later undecorated in the doctree, in relabel_references
58 references = set()
59 for line in lines:
60 line = line.strip()
61 m = re.match(
62 r"^\.\. +\[(%s)\]" % app.config.numpydoc_citation_re, line, re.IGNORECASE
63 )
64 if m:
65 references.add(m.group(1))
67 if references:
68 # we use a hash to mangle the reference name to avoid invalid names
69 sha = hashlib.sha256()
70 sha.update(name.encode("utf8"))
71 prefix = "R" + sha.hexdigest()[:HASH_LEN]
73 for r in references:
74 new_r = prefix + "-" + r
75 for i, line in enumerate(lines):
76 lines[i] = lines[i].replace(f"[{r}]_", f"[{new_r}]_")
77 lines[i] = lines[i].replace(f".. [{r}]", f".. [{new_r}]")
80def _is_cite_in_numpydoc_docstring(citation_node):
81 # Find DEDUPLICATION_TAG in comment as last node of sibling section
83 # XXX: I failed to use citation_node.traverse to do this:
84 section_node = citation_node.parent
86 def is_docstring_section(node):
87 return isinstance(node, (section, desc_content))
89 while not is_docstring_section(section_node):
90 section_node = section_node.parent
91 if section_node is None:
92 return False
94 sibling_sections = itertools.chain(
95 _traverse_or_findall(
96 section_node,
97 is_docstring_section,
98 include_self=True,
99 descend=False,
100 siblings=True,
101 )
102 )
103 for sibling_section in sibling_sections:
104 if not sibling_section.children:
105 continue
107 for child in sibling_section.children[::-1]:
108 if not isinstance(child, comment):
109 continue
111 if child.rawsource.strip() == DEDUPLICATION_TAG.strip():
112 return True
114 return False
117def relabel_references(app, doc):
118 # Change 'hash-ref' to 'ref' in label text
119 for citation_node in _traverse_or_findall(doc, citation):
120 if not _is_cite_in_numpydoc_docstring(citation_node):
121 continue
122 label_node = citation_node[0]
123 prefix, _, new_label = label_node[0].astext().partition("-")
124 assert len(prefix) == HASH_LEN + 1
125 new_text = Text(new_label)
126 label_node.replace(label_node[0], new_text)
128 for id_ in citation_node["backrefs"]:
129 ref = doc.ids[id_]
130 ref_text = ref[0]
132 # Sphinx has created pending_xref nodes with [reftext] text.
133 def matching_pending_xref(node):
134 return (
135 isinstance(node, pending_xref)
136 and node[0].astext() == f"[{ref_text}]"
137 )
139 for xref_node in _traverse_or_findall(ref.parent, matching_pending_xref):
140 xref_node.replace(xref_node[0], Text(f"[{new_text}]"))
141 ref.replace(ref_text, new_text.copy())
144def clean_backrefs(app, doc, docname):
145 # only::latex directive has resulted in citation backrefs without reference
146 known_ref_ids = set()
147 for ref in _traverse_or_findall(doc, reference, descend=True):
148 for id_ in ref["ids"]:
149 known_ref_ids.add(id_)
150 # some extensions produce backrefs to inline elements
151 for ref in _traverse_or_findall(doc, inline, descend=True):
152 for id_ in ref["ids"]:
153 known_ref_ids.add(id_)
154 for citation_node in _traverse_or_findall(doc, citation, descend=True):
155 # remove backrefs to non-existent refs
156 citation_node["backrefs"] = [
157 id_ for id_ in citation_node["backrefs"] if id_ in known_ref_ids
158 ]
161DEDUPLICATION_TAG = " !! processed by numpydoc !!"
164def mangle_docstrings(app, what, name, obj, options, lines):
165 if DEDUPLICATION_TAG in lines:
166 return
167 show_inherited_class_members = app.config.numpydoc_show_inherited_class_members
168 if isinstance(show_inherited_class_members, dict):
169 try:
170 show_inherited_class_members = show_inherited_class_members[name]
171 except KeyError:
172 show_inherited_class_members = True
174 cfg = {
175 "use_plots": app.config.numpydoc_use_plots,
176 "show_class_members": app.config.numpydoc_show_class_members,
177 "show_inherited_class_members": show_inherited_class_members,
178 "class_members_toctree": app.config.numpydoc_class_members_toctree,
179 "attributes_as_param_list": app.config.numpydoc_attributes_as_param_list,
180 "xref_param_type": app.config.numpydoc_xref_param_type,
181 "xref_aliases": app.config.numpydoc_xref_aliases_complete,
182 "xref_ignore": app.config.numpydoc_xref_ignore,
183 }
185 cfg.update(options or {})
186 u_NL = "\n"
187 if what == "module":
188 # Strip top title
189 pattern = "^\\s*[#*=]{4,}\\n[a-z0-9 -]+\\n[#*=]{4,}\\s*"
190 title_re = re.compile(pattern, re.IGNORECASE | re.DOTALL)
191 lines[:] = title_re.sub("", u_NL.join(lines)).split(u_NL)
192 else:
193 try:
194 doc = get_doc_object(
195 obj, what, u_NL.join(lines), config=cfg, builder=app.builder
196 )
197 lines[:] = str(doc).split(u_NL)
198 except Exception:
199 logger.error("[numpydoc] While processing docstring for %r", name)
200 raise
202 if app.config.numpydoc_validation_checks:
203 # If the user has supplied patterns to ignore via the
204 # numpydoc_validation_exclude config option, skip validation for
205 # any objs whose name matches any of the patterns
206 excluder = app.config.numpydoc_validation_excluder
207 exclude_from_validation = excluder.search(name) if excluder else False
208 if not exclude_from_validation:
209 # TODO: Currently, all validation checks are run and only those
210 # selected via config are reported. It would be more efficient to
211 # only run the selected checks.
212 report = validate(doc)
213 errors = [
214 err
215 for err in report["errors"]
216 if not (
217 (
218 overrides := app.config.numpydoc_validation_overrides.get(
219 err[0]
220 )
221 )
222 and re.search(overrides, report["docstring"])
223 )
224 ]
225 if {err[0] for err in errors} & app.config.numpydoc_validation_checks:
226 msg = (
227 f"[numpydoc] Validation warnings while processing "
228 f"docstring for {name!r}:\n"
229 )
230 for err in errors:
231 if err[0] in app.config.numpydoc_validation_checks:
232 msg += f" {err[0]}: {err[1]}\n"
233 logger.warning(msg)
235 # call function to replace reference numbers so that there are no
236 # duplicates
237 rename_references(app, what, name, obj, options, lines)
239 lines += ["..", DEDUPLICATION_TAG]
242def mangle_signature(app, what, name, obj, options, sig, retann):
243 # Do not try to inspect classes that don't define `__init__`
244 if inspect.isclass(obj) and (
245 not hasattr(obj, "__init__")
246 or "initializes x; see " in pydoc.getdoc(obj.__init__)
247 ):
248 return "", ""
250 if not (isinstance(obj, Callable) or hasattr(obj, "__argspec_is_invalid_")):
251 return None
253 if not hasattr(obj, "__doc__"):
254 return None
255 doc = get_doc_object(obj, config={"show_class_members": False})
256 sig = doc["Signature"] or _clean_text_signature(
257 getattr(obj, "__text_signature__", None)
258 )
259 if sig:
260 sig = re.sub("^[^(]*", "", sig)
261 return sig, ""
264def _clean_text_signature(sig):
265 if sig is None:
266 return None
267 start_pattern = re.compile(r"^[^(]*\(")
268 start, end = start_pattern.search(sig).span()
269 start_sig = sig[start:end]
270 sig = sig[end:-1]
271 sig = re.sub(r"^\$(self|module|type)(,\s|$)", "", sig, count=1)
272 sig = re.sub(r"(^|(?<=,\s))/,\s\*", "*", sig, count=1)
273 return start_sig + sig + ")"
276def setup(app, get_doc_object_=get_doc_object):
277 if not hasattr(app, "add_config_value"):
278 return None # probably called by nose, better bail out
280 global get_doc_object
281 get_doc_object = get_doc_object_
283 app.setup_extension("sphinx.ext.autosummary")
284 app.connect("config-inited", update_config)
285 app.connect("autodoc-process-docstring", mangle_docstrings)
286 app.connect("autodoc-process-signature", mangle_signature)
287 app.connect("doctree-read", relabel_references)
288 app.connect("doctree-resolved", clean_backrefs)
289 app.add_config_value("numpydoc_use_plots", None, False)
290 app.add_config_value("numpydoc_show_class_members", True, True)
291 app.add_config_value(
292 "numpydoc_show_inherited_class_members", True, True, types=(bool, dict)
293 )
294 app.add_config_value("numpydoc_class_members_toctree", True, True)
295 app.add_config_value("numpydoc_citation_re", "[a-z0-9_.-]+", True)
296 app.add_config_value("numpydoc_attributes_as_param_list", True, True)
297 app.add_config_value("numpydoc_xref_param_type", False, True)
298 app.add_config_value("numpydoc_xref_aliases", dict(), True)
299 app.add_config_value("numpydoc_xref_ignore", set(), True)
300 app.add_config_value("numpydoc_validation_checks", set(), True)
301 app.add_config_value("numpydoc_validation_exclude", set(), False)
302 app.add_config_value("numpydoc_validation_overrides", dict(), False)
304 # Extra mangling domains
305 app.add_domain(NumpyPythonDomain)
306 app.add_domain(NumpyCDomain)
308 metadata = {"version": __version__, "parallel_read_safe": True}
309 return metadata
312def update_config(app, config=None):
313 """Update the configuration with default values."""
314 if config is None: # needed for testing and old Sphinx
315 config = app.config
316 # Do not simply overwrite the `app.config.numpydoc_xref_aliases`
317 # otherwise the next sphinx-build will compare the incoming values (without
318 # our additions) to the old values (with our additions) and trigger
319 # a full rebuild!
320 numpydoc_xref_aliases_complete = deepcopy(config.numpydoc_xref_aliases)
321 for key, value in DEFAULT_LINKS.items():
322 if key not in numpydoc_xref_aliases_complete:
323 numpydoc_xref_aliases_complete[key] = value
324 config.numpydoc_xref_aliases_complete = numpydoc_xref_aliases_complete
326 # Processing to determine whether numpydoc_validation_checks is treated
327 # as a blocklist or allowlist
328 config.numpydoc_validation_checks = get_validation_checks(
329 config.numpydoc_validation_checks
330 )
332 # Generate the regexp for docstrings to ignore during validation
333 if isinstance(config.numpydoc_validation_exclude, str):
334 raise ValueError(
335 f"numpydoc_validation_exclude must be a container of strings, "
336 f"e.g. [{config.numpydoc_validation_exclude!r}]."
337 )
338 config.numpydoc_validation_excluder = None
339 if config.numpydoc_validation_exclude:
340 exclude_expr = re.compile(
341 r"|".join(exp for exp in config.numpydoc_validation_exclude)
342 )
343 config.numpydoc_validation_excluder = exclude_expr
345 for check, patterns in config.numpydoc_validation_overrides.items():
346 config.numpydoc_validation_overrides[check] = re.compile(
347 r"|".join(exp for exp in patterns)
348 )
351# ------------------------------------------------------------------------------
352# Docstring-mangling domains
353# ------------------------------------------------------------------------------
355from docutils.statemachine import ViewList
356from sphinx.domains.c import CDomain
357from sphinx.domains.python import PythonDomain
360class ManglingDomainBase:
361 directive_mangling_map = {}
363 def __init__(self, *a, **kw):
364 super().__init__(*a, **kw)
365 self.wrap_mangling_directives()
367 def wrap_mangling_directives(self):
368 for name, objtype in list(self.directive_mangling_map.items()):
369 self.directives[name] = wrap_mangling_directive(
370 self.directives[name], objtype
371 )
374class NumpyPythonDomain(ManglingDomainBase, PythonDomain):
375 name = "np"
376 directive_mangling_map = {
377 "function": "function",
378 "class": "class",
379 "exception": "class",
380 "method": "function",
381 "classmethod": "function",
382 "staticmethod": "function",
383 "attribute": "attribute",
384 }
385 indices = []
388class NumpyCDomain(ManglingDomainBase, CDomain):
389 name = "np-c"
390 directive_mangling_map = {
391 "function": "function",
392 "member": "attribute",
393 "macro": "function",
394 "type": "class",
395 "var": "object",
396 }
399def match_items(lines, content_old):
400 """Create items for mangled lines.
402 This function tries to match the lines in ``lines`` with the items (source
403 file references and line numbers) in ``content_old``. The
404 ``mangle_docstrings`` function changes the actual docstrings, but doesn't
405 keep track of where each line came from. The mangling does many operations
406 on the original lines, which are hard to track afterwards.
408 Many of the line changes come from deleting or inserting blank lines. This
409 function tries to match lines by ignoring blank lines. All other changes
410 (such as inserting figures or changes in the references) are completely
411 ignored, so the generated line numbers will be off if ``mangle_docstrings``
412 does anything non-trivial.
414 This is a best-effort function and the real fix would be to make
415 ``mangle_docstrings`` actually keep track of the ``items`` together with
416 the ``lines``.
418 Examples
419 --------
420 >>> lines = ["", "A", "", "B", " ", "", "C", "D"]
421 >>> lines_old = ["a", "", "", "b", "", "c"]
422 >>> items_old = [
423 ... ("file1.py", 0),
424 ... ("file1.py", 1),
425 ... ("file1.py", 2),
426 ... ("file2.py", 0),
427 ... ("file2.py", 1),
428 ... ("file2.py", 2),
429 ... ]
430 >>> content_old = ViewList(lines_old, items=items_old)
431 >>> match_items(lines, content_old) # doctest: +NORMALIZE_WHITESPACE
432 [('file1.py', 0), ('file1.py', 0), ('file2.py', 0), ('file2.py', 0),
433 ('file2.py', 2), ('file2.py', 2), ('file2.py', 2), ('file2.py', 2)]
434 >>> # first 2 ``lines`` are matched to 'a', second 2 to 'b', rest to 'c'
435 >>> # actual content is completely ignored.
437 Notes
438 -----
439 The algorithm tries to match any line in ``lines`` with one in
440 ``lines_old``. It skips over all empty lines in ``lines_old`` and assigns
441 this line number to all lines in ``lines``, unless a non-empty line is
442 found in ``lines`` in which case it goes to the next line in ``lines_old``.
444 """
445 items_new = []
446 lines_old = content_old.data
447 items_old = content_old.items
448 j = 0
449 for i, line in enumerate(lines):
450 # go to next non-empty line in old:
451 # line.strip() checks whether the string is all whitespace
452 while j < len(lines_old) - 1 and not lines_old[j].strip():
453 j += 1
454 items_new.append(items_old[j])
455 if line.strip() and j < len(lines_old) - 1:
456 j += 1
457 assert len(items_new) == len(lines)
458 return items_new
461def wrap_mangling_directive(base_directive, objtype):
462 class directive(base_directive):
463 def run(self):
464 env = self.state.document.settings.env
466 name = None
467 if self.arguments:
468 m = re.match(r"^(.*\s+)?(.*?)(\(.*)?", self.arguments[0])
469 name = m.group(2).strip()
471 if not name:
472 name = self.arguments[0]
474 lines = list(self.content)
475 mangle_docstrings(env.app, objtype, name, None, None, lines)
476 if self.content:
477 items = match_items(lines, self.content)
478 self.content = ViewList(lines, items=items, parent=self.content.parent)
480 return base_directive.run(self)
482 return directive