From f9c35357ddd224fcd954e4bad812be4f4e6bda6a Mon Sep 17 00:00:00 2001 From: Hood Chatham Date: Thu, 19 Jan 2023 14:37:33 -0800 Subject: [PATCH] More robust determination of rtype location / fix issue 302 Resolves 302. This increases the robustness of the handling of the docutils tree. The docutils rst parser seems to have a bunch of bugs / shortcomings. The original problem was that several list types do not get their line number set. So to find the line number of a node, if the node itself has None as the line number, we descend through the first children of the node until we find one with a non-None line number. --- src/sphinx_autodoc_typehints/__init__.py | 61 +++++++++++++----------- tests/roots/test-dummy/dummy_module.py | 12 +++++ tests/roots/test-dummy/index.rst | 2 + tests/test_sphinx_autodoc_typehints.py | 13 +++++ 4 files changed, 59 insertions(+), 29 deletions(-) diff --git a/src/sphinx_autodoc_typehints/__init__.py b/src/sphinx_autodoc_typehints/__init__.py index c079775e..ff59a952 100644 --- a/src/sphinx_autodoc_typehints/__init__.py +++ b/src/sphinx_autodoc_typehints/__init__.py @@ -659,10 +659,23 @@ class InsertIndexInfo: PARAM_SYNONYMS = ("param ", "parameter ", "arg ", "argument ", "keyword ", "kwarg ", "kwparam ") -def line_before_node(node: Node) -> int: - line = node.line - assert line - return line - 2 +def node_line_no(node: Node) -> int | None: + """ + Get the 1-indexed line on which the node starts if possible. If not, return + None. + + Descend through the first children until we locate one with a line number or + return None if None of them have one. + + I'm not aware of any rst on which this returns None, to find out would + require a more detailed analysis of the docutils rst parser source code. An + example where the node doesn't have a line number but the first child does + is all `definition_list` nodes. It seems like bullet_list and option_list + get line numbers, but enum_list also doesn't. *shrug*. + """ + while node.line is None and node.children: + node = node.children[0] + return node.line def tag_name(node: Node) -> str: @@ -690,39 +703,29 @@ def get_insert_index(app: Sphinx, lines: list[str]) -> InsertIndexInfo | None: # Find a top level child which is a field_list that contains a field whose # name starts with one of the PARAM_SYNONYMS. This is the parameter list. We # hope there is at most of these. - for idx, child in enumerate(doc.children): + for child in doc.children: if tag_name(child) != "field_list": continue - if any(c.children[0].astext().startswith(PARAM_SYNONYMS) for c in child.children): - idx = idx - break - else: - idx = -1 + if not any(c.children[0].astext().startswith(PARAM_SYNONYMS) for c in child.children): + continue - if idx == -1: - # No parameters - pass - elif idx + 1 < len(doc.children): - # Unfortunately docutils only tells us the line numbers that nodes start on, - # not the range (boo!). So insert before the line before the next sibling. - at = line_before_node(doc.children[idx + 1]) + # Found it! Try to insert before the next sibling. If there is no next + # sibling, insert at end. + # If there is a next sibling but we can't locate a line number, insert + # at end. (I don't know of any input where this happens.) + next_sibling = child.next_node(descend=False, siblings=True) + line_no = node_line_no(next_sibling) if next_sibling else None + at = line_no - 2 if line_no else len(lines) return InsertIndexInfo(insert_index=at, found_param=True) - else: - # No next sibling, insert at end - return InsertIndexInfo(insert_index=len(lines), found_param=True) # 4. Insert before examples # TODO: Maybe adjust which tags to insert ahead of - for idx, child in enumerate(doc.children): - if tag_name(child) not in ["literal_block", "paragraph", "field_list"]: - idx = idx - break - else: - idx = -1 - - if idx != -1: - at = line_before_node(doc.children[idx]) + for child in doc.children: + if tag_name(child) in ["literal_block", "paragraph", "field_list"]: + continue + line_no = node_line_no(child) + at = line_no - 2 if line_no else len(lines) return InsertIndexInfo(insert_index=at, found_directive=True) # 5. Otherwise, insert at end diff --git a/tests/roots/test-dummy/dummy_module.py b/tests/roots/test-dummy/dummy_module.py index e4219ee7..d7921930 100644 --- a/tests/roots/test-dummy/dummy_module.py +++ b/tests/roots/test-dummy/dummy_module.py @@ -395,3 +395,15 @@ def func_with_code_block() -> int: Here are a couple of examples of how to use this function. """ + + +def func_with_definition_list() -> int: + """Some text and then a definition list. + + abc + x + + xyz + something + """ + # See https://github.com/tox-dev/sphinx-autodoc-typehints/issues/302 diff --git a/tests/roots/test-dummy/index.rst b/tests/roots/test-dummy/index.rst index 221371dc..98d967f1 100644 --- a/tests/roots/test-dummy/index.rst +++ b/tests/roots/test-dummy/index.rst @@ -55,3 +55,5 @@ Dummy Module .. autofunction:: dummy_module.empty_line_between_parameters .. autofunction:: dummy_module.func_with_code_block + +.. autofunction:: dummy_module.func_with_definition_list diff --git a/tests/test_sphinx_autodoc_typehints.py b/tests/test_sphinx_autodoc_typehints.py index bd67eeb2..36bc4093 100644 --- a/tests/test_sphinx_autodoc_typehints.py +++ b/tests/test_sphinx_autodoc_typehints.py @@ -880,6 +880,19 @@ class dummy_module.TestClassAttributeDocs -[ Examples ]- Here are a couple of examples of how to use this function. + + dummy_module.func_with_definition_list() + + Some text and then a definition list. + + Return type: + "int" + + abc + x + + xyz + something """ expected_contents = dedent(expected_contents).format(**format_args).replace("–", "--") assert text_contents == maybe_fix_py310(expected_contents)