From 9b142974411e1e399e1762bb43c122a9344b17ef Mon Sep 17 00:00:00 2001 From: Tim Nordell Date: Wed, 29 Oct 2025 09:24:44 -0500 Subject: [PATCH] functions: Fix broken parent-child node relationships The find_and_replace_node_content function breaks parent-child node relationships since it does not go through the docutils tools for assigning node children. The Element class in docutils will [1] iterate over each added child; we'll mimic that here. (We can't use the Element variant since we're only guaranteed to be of type Node, which doesn't have these helper functions, unfortunately.) Notably, the sphinxcontrib-spelling [2] extension is broken by this broken parent-child relationship since if something is misspelled it tries to identify which source line was impacted. When a node is added as a child, setup_child(...) [3] code fixes up the source/line location with the parent's information if needed. A test has been added at the top-level which covers every single test run by adding a build hook into the "doctree-resolved" hook. This test is fairly simple - check each node (outside of the top node) and validate that it has a parent assigned. [1] https://github.com/docutils/docutils/blob/4e912fe000b1b6dc1466c0ad1c3d1787d40fd96d/docutils/docutils/nodes.py#L788-L794 [2] https://pypi.org/project/sphinxcontrib-spelling/ [3] https://github.com/docutils/docutils/blob/4e912fe000b1b6dc1466c0ad1c3d1787d40fd96d/docutils/docutils/nodes.py#L150-L157 --- sphinx_needs/functions/functions.py | 4 ++++ tests/conftest.py | 9 +++++++++ 2 files changed, 13 insertions(+) diff --git a/sphinx_needs/functions/functions.py b/sphinx_needs/functions/functions.py index 873f6e23a..197b94250 100644 --- a/sphinx_needs/functions/functions.py +++ b/sphinx_needs/functions/functions.py @@ -257,6 +257,8 @@ def find_and_replace_node_content( new_child = find_and_replace_node_content(child, env, need) new_children.append(new_child) node.children = new_children + for subchild in node.children: + node.setup_child(subchild) else: node = nodes.Text(new_text) return node @@ -269,6 +271,8 @@ def find_and_replace_node_content( new_child = find_and_replace_node_content(child, env, need) new_children.append(new_child) node.children = new_children + for subchild in node.children: + node.setup_child(subchild) return node diff --git a/tests/conftest.py b/tests/conftest.py index 72b8631ad..5537e58e5 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -254,6 +254,13 @@ def sphinx_test_tempdir(request) -> path: return sphinx_test_tempdir +def test_check_parent_child(app: Sphinx, doctree: document, docname: str): + for idx, node in enumerate(doctree.findall()): + if idx == 0: + continue + assert node.parent + + @pytest.fixture(scope="function") def test_app(make_app, sphinx_test_tempdir, request): """ @@ -330,6 +337,8 @@ def test_app(make_app, sphinx_test_tempdir, request): # ``test_js`` behaves like a method. app.test_js = test_js.__get__(app, Sphinx) + app.connect("doctree-resolved", test_check_parent_child, priority=999) + yield app app.cleanup()