diff --git a/numpydoc/numpydoc.py b/numpydoc/numpydoc.py index 82100062..e656fb5c 100644 --- a/numpydoc/numpydoc.py +++ b/numpydoc/numpydoc.py @@ -43,6 +43,19 @@ HASH_LEN = 12 +def _traverse_or_findall(node, condition, **kwargs): + """Triage node.traverse (docutils <0.18.1) vs node.findall. + + TODO: This check can be removed when the minimum supported docutils version + for numpydoc is docutils>=0.18.1 + """ + return ( + node.findall(condition, **kwargs) + if hasattr(node, "findall") + else node.traverse(condition, **kwargs) + ) + + def rename_references(app, what, name, obj, options, lines): # decorate reference numbers so that there are no duplicates # these are later undecorated in the doctree, in relabel_references @@ -81,8 +94,12 @@ def is_docstring_section(node): return False sibling_sections = itertools.chain( - section_node.traverse( - is_docstring_section, include_self=True, descend=False, siblings=True + _traverse_or_findall( + section_node, + is_docstring_section, + include_self=True, + descend=False, + siblings=True, ) ) for sibling_section in sibling_sections: @@ -101,7 +118,7 @@ def is_docstring_section(node): def relabel_references(app, doc): # Change 'hash-ref' to 'ref' in label text - for citation_node in doc.traverse(citation): + for citation_node in _traverse_or_findall(doc, citation): if not _is_cite_in_numpydoc_docstring(citation_node): continue label_node = citation_node[0] @@ -121,7 +138,7 @@ def matching_pending_xref(node): and node[0].astext() == f"[{ref_text}]" ) - for xref_node in ref.parent.traverse(matching_pending_xref): + for xref_node in _traverse_or_findall(ref.parent, matching_pending_xref): xref_node.replace(xref_node[0], Text(f"[{new_text}]")) ref.replace(ref_text, new_text.copy()) @@ -129,10 +146,10 @@ def matching_pending_xref(node): def clean_backrefs(app, doc, docname): # only::latex directive has resulted in citation backrefs without reference known_ref_ids = set() - for ref in doc.traverse(reference, descend=True): + for ref in _traverse_or_findall(doc, reference, descend=True): for id_ in ref["ids"]: known_ref_ids.add(id_) - for citation_node in doc.traverse(citation, descend=True): + for citation_node in _traverse_or_findall(doc, citation, descend=True): # remove backrefs to non-existent refs citation_node["backrefs"] = [ id_ for id_ in citation_node["backrefs"] if id_ in known_ref_ids diff --git a/numpydoc/tests/test_full.py b/numpydoc/tests/test_full.py index e0defe2e..c4ae1340 100644 --- a/numpydoc/tests/test_full.py +++ b/numpydoc/tests/test_full.py @@ -1,11 +1,13 @@ import os.path as op import re import shutil +from packaging import version import pytest import sphinx from sphinx.application import Sphinx from sphinx.util.docutils import docutils_namespace +from docutils import __version__ as docutils_version # Test framework adapted from sphinx-gallery (BSD 3-clause) @@ -89,7 +91,14 @@ def test_reference(sphinx_app, html_file, expected_length): with open(op.join(out_dir, *html_file)) as fid: html = fid.read() - reference_list = re.findall(r'(.*)<\/a>', html) + # TODO: This check can be removed when the minimum supported docutils version + # for numpydoc is docutils>=0.18 + pattern = ( + 'role="doc-backlink"' + if version.parse(docutils_version) >= version.parse("0.18") + else 'class="fn-backref"' + ) + reference_list = re.findall(rf'(.*)<\/a>', html) assert len(reference_list) == expected_length for ref in reference_list: