Repository: useblocks/sphinx-needs
Pull request #1250 (2024-08-28 04:54:50)

👌 Improve need warnings
- remove "fix" that is now fixed in sphinx (https://github.com/sphinx-doc/sphinx/commit/6065ab676c08fe37b84bfd0a0146ab08754c8b92)
- add subtype to to warnings
- improve test to check entire warning string

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/useblocks/sphinx-needs/pull/1250?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=useblocks) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 86.51%. Comparing base [(`4e10030`)](https://app.codecov.io/gh/useblocks/sphinx-needs/commit/4e10030581c03c59764e517ab750d3ddbb2695d8?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=useblocks) to head [(`946d746`)](https://app.codecov.io/gh/useblocks/sphinx-needs/commit/946d7467b9620ba3ea0dc5b33a95df4899294879?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=useblocks).
> Report is 16 commits behind head on master.

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##           master    #1250      +/-   ##
==========================================
- Coverage   86.87%   86.51%   -0.37%     
==========================================
  Files          56       60       +4     
  Lines        6532     6906     +374     
==========================================
+ Hits         5675     5975     +300     
- Misses        857      931      +74     
```

| [Flag](https://app.codecov.io/gh/useblocks/sphinx-needs/pull/1250/flags?src=pr&el=flags&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=useblocks) | Coverage Δ | |
|---|---|---|
| [pytests](https://app.codecov.io/gh/useblocks/sphinx-needs/pull/1250/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=useblocks) | `86.51% <ø> (-0.37%)` | :arrow_down: |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=useblocks#carryforward-flags-in-the-pull-request-comment) to find out more.

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/useblocks/sphinx-needs/pull/1250?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=useblocks).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=useblocks).


Patch:
diff --git a/docs/directives/needextract.rst b/docs/directives/needextract.rst
index 5073a30a..2b28e21b 100644
--- a/docs/directives/needextract.rst
+++ b/docs/directives/needextract.rst
@@ -23,12 +23,17 @@ For instance, a supplier could get a copy of requirements but would not see all
       :style: green_border
 
 
-.. note:: 
+.. caution::
+
+   ``needextract`` supports correct rendering of most, but not all, content coming from the original need.
+   Be careful when using complex content in the original need, like custom roles or directives
+   that require any sphinx transforms.
    
-   **needextract** supports the full filtering possibilities of **Sphinx-Needs**.
-   Please read :ref:`filter` for more information.
+``needextract`` supports the full filtering possibilities of sphinx-needs.
+Please read :ref:`filter` for more information.
 
-``needextract`` supports also arguments as filter string. It works like the option `filter`, but also
+It also supports arguments as filter string,
+which works like the option `filter`, but also
 supports need ID as filter argument.
 
 .. need-example::
diff --git a/sphinx_needs/builder.py b/sphinx_needs/builder.py
index 8ecd729a..d7514914 100644
--- a/sphinx_needs/builder.py
+++ b/sphinx_needs/builder.py
@@ -4,7 +4,6 @@ import os
 from typing import Iterable, Sequence
 
 from docutils import nodes
-from sphinx import version_info
 from sphinx.application import Sphinx
 from sphinx.builders import Builder
 
@@ -286,12 +285,7 @@ def build_needumls_pumls(app: Sphinx, _exception: Exception) -> None:
         return
 
     # if other builder like html used together with config: needs_build_needumls
-    if version_info[0] >= 5:
-        needs_builder = NeedumlsBuilder(app, env)
-        needs_builder.outdir = os.path.join(needs_builder.outdir, config.build_needumls)
-    else:
-        needs_builder = NeedumlsBuilder(app)
-        needs_builder.outdir = os.path.join(needs_builder.outdir, config.build_needumls)
-        needs_builder.set_environment(env)
+    needs_builder = NeedumlsBuilder(app, env)
+    needs_builder.outdir = os.path.join(needs_builder.outdir, config.build_needumls)
 
     needs_builder.finish()
diff --git a/sphinx_needs/directives/need.py b/sphinx_needs/directives/need.py
index c18c8665..568c97b6 100644
--- a/sphinx_needs/directives/need.py
+++ b/sphinx_needs/directives/need.py
@@ -25,7 +25,7 @@ from sphinx_needs.functions import (
     resolve_variants_options,
 )
 from sphinx_needs.functions.functions import check_and_get_content
-from sphinx_needs.layout import build_need
+from sphinx_needs.layout import build_need_repr
 from sphinx_needs.logging import get_logger, log_warning
 from sphinx_needs.need_constraints import process_constraints
 from sphinx_needs.nodes import Need
@@ -434,15 +434,18 @@ def format_need_nodes(
         need_id = node_need.attributes["ids"][0]
         need_data = needs[need_id]
 
+        if need_data["hide"]:
+            remove_node_from_tree(node_need)
+            continue
+
         find_and_replace_node_content(node_need, env, need_data)
         for index, attribute in enumerate(node_need.attributes["classes"]):
             node_need.attributes["classes"][index] = check_and_get_content(
                 attribute, need_data, env
             )
 
-        layout = need_data["layout"] or NeedsSphinxConfig(app.config).default_layout
-
-        build_need(layout, node_need, app, fromdocname=fromdocname)
+        rendered_node = build_need_repr(node_need, need_data, app, docname=fromdocname)
+        node_need.parent.replace(node_need, rendered_node)
 
 
 def check_links(needs: dict[str, NeedsInfoType], config: NeedsSphinxConfig) -> None:
diff --git a/sphinx_needs/directives/needextract.py b/sphinx_needs/directives/needextract.py
index 42ba6772..5742cf33 100644
--- a/sphinx_needs/directives/needextract.py
+++ b/sphinx_needs/directives/needextract.py
@@ -1,26 +1,29 @@
 from __future__ import annotations
 
 import re
-from typing import Sequence, cast
+from typing import Sequence
 
 from docutils import nodes
 from docutils.parsers.rst import directives
 from docutils.transforms.references import Substitutions
 from sphinx.application import Sphinx
 from sphinx.environment.collectors.asset import DownloadFileCollector, ImageCollector
+from sphinx.util.logging import getLogger
 
-from sphinx_needs.api.exceptions import NeedsInvalidFilter
 from sphinx_needs.config import NeedsSphinxConfig
-from sphinx_needs.data import NeedsExtractType, SphinxNeedsData
+from sphinx_needs.data import NeedsExtractType, NeedsInfoType, SphinxNeedsData
 from sphinx_needs.debug import measure_time
 from sphinx_needs.directives.utils import (
     no_needs_found_paragraph,
     used_filter_paragraph,
 )
 from sphinx_needs.filter_common import FilterBase, process_filters
-from sphinx_needs.layout import SphinxNeedLayoutException, build_need
+from sphinx_needs.layout import build_need_repr
+from sphinx_needs.logging import log_warning
 from sphinx_needs.utils import add_doc, remove_node_from_tree
 
+LOGGER = getLogger(__name__)
+
 
 class Needextract(nodes.General, nodes.Element):
     pass
@@ -82,30 +85,42 @@ def process_needextract(
     env = app.env
     needs_config = NeedsSphinxConfig(app.config)
 
-    for node in found_nodes:
+    node: Needextract
+    for node in found_nodes:  # type: ignore[assignment]
         if not needs_config.include_needs:
             remove_node_from_tree(node)
             continue
 
         current_needextract: NeedsExtractType = node.attributes
         all_needs = SphinxNeedsData(env).get_or_create_needs()
-        content: list[nodes.Element] = []
+        content = nodes.container()
+        content.attributes["ids"] = [current_needextract["target_id"]]
 
         # check if filter argument and option filter both exist
         need_filter_arg = current_needextract["filter_arg"]
         if need_filter_arg and current_needextract["filter"]:
-            raise NeedsInvalidFilter(
-                "Needextract can't have filter arguments and option filter at the same time."
+            log_warning(
+                LOGGER,
+                "filter arguments and option filter at the same time are disallowed.",
+                "needextract",
+                location=node,
             )
+            remove_node_from_tree(node)
+            continue
         elif need_filter_arg:
             # check if given filter argument is need-id
             if need_filter_arg in all_needs:
                 need_filter_arg = f'id == "{need_filter_arg}"'
             elif re.fullmatch(needs_config.id_regex, need_filter_arg):
                 # check if given filter argument is need-id, but not exists
-                raise NeedsInvalidFilter(
-                    f"Provided id {need_filter_arg} for needextract does not exist."
+                log_warning(
+                    LOGGER,
+                    f"Requested need {need_filter_arg!r} not found.",
+                    "needextract",
+                    location=node,
                 )
+                remove_node_from_tree(node)
+                continue
             current_needextract["filter"] = need_filter_arg
 
         found_needs = process_filters(
@@ -119,18 +134,15 @@ def process_needextract(
         for need_info in found_needs:
             # filter out need_part from found_needs, in order to generate
             # copies of filtered needs with custom layout and style
-            if need_info["is_need"] and not need_info["is_part"]:
-                need_extract = _build_needextract(
-                    need_info["id"],
-                    app,
-                    layout=current_needextract["layout"],
-                    style=current_needextract["style"],
-                    docname=current_needextract["docname"],
+            if (
+                need_info["is_need"]
+                and not need_info["is_part"]
+                and (
+                    need_extract := _build_needextract(
+                        app, node, need_info, current_needextract
+                    )
                 )
-
-                # Add lineno to node
-                need_extract.line = current_needextract["lineno"]
-
+            ):
                 content.append(need_extract)
 
         if len(content) == 0:
@@ -152,87 +164,54 @@ def process_needextract(
 
 @measure_time("build_needextract")
 def _build_needextract(
-    need_id: str,
     app: Sphinx,
-    layout: str | None = None,
-    style: str | None = None,
-    docname: str | None = None,
-) -> nodes.container:
-    """
-    Creates a new need-node for a given layout.
-
-    Need must already exist in internal dictionary.
-    This creates a new representation only.
-    :param need_id: need id
-    :param app: sphinx application
-    :param layout: layout to use, overrides layout set by need itself
-    :param style: style to use, overrides styles set by need itself
-    :param docname: Needed for calculating references
-    :return:
-    """
+    extract_node: Needextract,
+    need_data: NeedsInfoType,
+    extract_data: NeedsExtractType,
+) -> nodes.container | None:
+    """Creates a new need representation."""
     env = app.env
-    needs = SphinxNeedsData(env).get_or_create_needs()
-
-    if need_id not in needs.keys():
-        raise SphinxNeedLayoutException(f"Given need id {need_id} does not exist.")
-
-    need_data = needs[need_id]
-
-    # Resolve internal references.
-    # This is done for original need content automatically.
-    # But as we are working on  a copy, we have to trigger this on our own.
-    if docname is None:
-        # needed to calculate relative references
-        # TODO ideally we should not cast here:
-        # the docname can still be None, if the need is external, although practically these are not rendered
-        docname = cast(str, needs[need_id]["docname"])
 
-    node_container = nodes.container()
-    # node_container += needs[need_id]["need_node"].children
-
-    node_inner = SphinxNeedsData(env).get_need_node(need_id)
-    assert node_inner is not None, f"Need {need_id} has no content node."
+    if (need_node := SphinxNeedsData(env).get_need_node(need_data["id"])) is None:
+        log_warning(
+            LOGGER,
+            f"Content for requested need {need_data['id']!r} not found.",
+            "needextract",
+            location=extract_node,
+        )
+        return None
 
-    # Rerun some important Sphinx collectors for need-content coming from "needsexternal".
-    # This is needed, as Sphinx needs to know images and download paths.
-    # Normally this gets done much earlier in the process, so that for the copied need-content this
-    # handling was and will not be done by Sphinx itself anymore.
+    dummy_need = nodes.container()
+    dummy_need.source, dummy_need.line = extract_node.source, extract_node.line
 
-    # Overwrite the docname, which must be the original one from the reused need, as all used paths are relative
-    # to the original location, not to the current document.
+    # Try to implement Sphinx transforms that would have already been done if the need was in the original document.
+    # Note, this is very hacky and can not possibly account for all transforms.
     env.temp_data["docname"] = need_data[
         "docname"
-    ]  # Dirty, as in this phase normally no docname is set anymore in env
-    ImageCollector().process_doc(app, node_inner)  # type: ignore[arg-type]
-    DownloadFileCollector().process_doc(app, node_inner)  # type: ignore[arg-type]
-
+    ]  # this is normally set in the read phase
+    ImageCollector().process_doc(app, need_node)  # type: ignore[arg-type]
+    DownloadFileCollector().process_doc(app, need_node)  # type: ignore[arg-type]
     del env.temp_data["docname"]  # Be sure our env is as it was before
 
-    node_container.append(node_inner)
+    dummy_need.extend(need_node.children)
 
     # resolve_references() ignores the given docname and takes the docname from the pending_xref node.
     # Therefore, we need to manipulate this first, before we can ask Sphinx to perform the normal
     # reference handling for us.
-    _replace_pending_xref_refdoc(node_container, docname)
-    env.resolve_references(node_container, docname, env.app.builder)  # type: ignore[arg-type]
-
-    node_container.attributes["ids"].append(need_id)
-
-    needs_config = NeedsSphinxConfig(app.config)
-    layout = layout or need_data["layout"] or needs_config.default_layout
-    style = style or need_data["style"] or needs_config.default_style
-
-    build_need(layout, node_container, app, style, docname)
-
-    # set the layout and style for the new need
-    node_container[0].attributes = node_container.parent.children[0].attributes  # type: ignore
-    node_container[0].children[0].attributes = (  # type: ignore
-        node_container.parent.children[0].children[0].attributes  # type: ignore
+    _replace_pending_xref_refdoc(dummy_need, extract_data["docname"])
+    env.resolve_references(dummy_need, extract_data["docname"], app.builder)  # type: ignore[arg-type]
+
+    dummy_need.attributes["ids"].append(need_data["id"])
+    rendered_node = build_need_repr(
+        dummy_need,  # type: ignore[arg-type]
+        need_data,
+        app,
+        layout=extract_data["layout"],
+        style=extract_data["style"],
+        docname=extract_data["docname"],
     )
 
-    node_container.attributes["ids"] = []
-
-    return node_container
+    return rendered_node
 
 
 def _replace_pending_xref_refdoc(node: nodes.Element, new_refdoc: str) -> None:
diff --git a/sphinx_needs/layout.py b/sphinx_needs/layout.py
index 2e12561f..beb54ce4 100644
--- a/sphinx_needs/layout.py
+++ b/sphinx_needs/layout.py
@@ -27,26 +27,32 @@ from sphinx.application import Sphinx
 from sphinx.util.logging import getLogger
 
 from sphinx_needs.config import NeedsSphinxConfig
-from sphinx_needs.data import NeedsCoreFields, NeedsInfoType, SphinxNeedsData
+from sphinx_needs.data import NeedsCoreFields, NeedsInfoType
 from sphinx_needs.debug import measure_time
 from sphinx_needs.logging import log_warning
+from sphinx_needs.nodes import Need
 from sphinx_needs.utils import match_string_link
 
 LOGGER = getLogger(__name__)
 
 
-@measure_time("need")
-def build_need(
-    layout: str,
-    node: nodes.Element,
+@measure_time("build_need_repr")
+def build_need_repr(
+    node: Need,
+    data: NeedsInfoType,
     app: Sphinx,
+    *,
+    layout: str | None = None,
     style: str | None = None,
-    fromdocname: str | None = None,
-) -> None:
-    """
-    Builds a need based on a given layout for a given need-node.
+    docname: str | None = None,
+) -> nodes.container:
+    """Create an output representation for a need.
+
+    :param layout: Override layout from need data / config
+    :param style: Override style from need data / config
+    :param docname: Override docname from need data / config
 
-    The created table must have the following docutils structure::
+    The created table will have the following docutils structure::
 
         - table
         -- tgroup
@@ -59,23 +65,9 @@ def build_need(
 
     The level structure must be kept, otherwise docutils can not handle it!
     """
-
-    env = app.env
-    needs = SphinxNeedsData(env).get_or_create_needs()
     node_container = nodes.container()
 
-    need_id = node.attributes["ids"][0]
-    need_data = needs[need_id]
-
-    if need_data["hide"]:
-        if node.parent:
-            node.parent.replace(node, [])
-        return
-
-    if fromdocname is None:
-        fromdocname = need_data["docname"]
-
-    lh = LayoutHandler(app, need_data, layout, node, style, fromdocname)
+    lh = LayoutHandler(app, data, node, layout=layout, style=style, docname=docname)
     new_need_node = lh.get_need_table()
     node_container.append(new_need_node)
 
@@ -83,9 +75,7 @@ def build_need(
     node_container.attributes["ids"] = [container_id]
     node_container.attributes["classes"] = ["need_container"]
 
-    # We need to replace the current need-node (containing content only) with our new table need node.
-    # node.parent.replace(node, node_container)
-    node.parent.replace(node, node_container)
+    return node_container
 
 
 @lru_cache(1)
@@ -105,16 +95,25 @@ class LayoutHandler:
         self,
         app: Sphinx,
         need: NeedsInfoType,
-        layout: str,
-        node: nodes.Element,
+        node: Need,
+        *,
+        layout: str | None = None,
         style: str | None = None,
-        fromdocname: str | None = None,
+        docname: str | None = None,
     ) -> None:
+        """
+
+        :param layout: Override layout from need data / config
+        :param style: Override style from need data / config
+        :param docname: Override docname from need data / config
+        """
         self.app = app
         self.need = need
         self.needs_config = NeedsSphinxConfig(app.config)
 
-        self.layout_name = layout
+        self.layout_name = (
+            layout or self.need["layout"] or self.needs_config.default_layout
+        )
         available_layouts = self.needs_config.layouts
         if self.layout_name not in available_layouts:
             raise SphinxNeedLayoutException(
@@ -127,10 +126,7 @@ class LayoutHandler:
         self.node = node
 
         # Used, if you need is referenced from another page
-        if fromdocname is None:
-            self.fromdocname = need["docname"]
-        else:
-            self.fromdocname = fromdocname
+        self.fromdocname = need["docname"] if docname is None else docname
 
         classes = [
             "need",
diff --git a/sphinx_needs/warnings.py b/sphinx_needs/warnings.py
index 94346f3b..7d010d07 100644
--- a/sphinx_needs/warnings.py
+++ b/sphinx_needs/warnings.py
@@ -81,15 +81,6 @@ def process_warnings(app: Sphinx, exception: Exception | None) -> None:
             else:
                 need_ids = [x["id"] for x in result]
 
-                # Set Sphinx statuscode to 1, only if -W is used with sphinx-build
-                # Because Sphinx statuscode got calculated in very early build phase and based on warning_count
-                # Sphinx-needs warnings check hasn't happened yet
-                # see deatils in https://github.com/sphinx-doc/sphinx/blob/81a4fd973d4cfcb25d01a7b0be62cdb28f82406d/sphinx/application.py#L345
-                # To be clear, app.keep_going = -W and --keep-going, and will overrite -W after
-                # see details in https://github.com/sphinx-doc/sphinx/blob/4.x/sphinx/application.py#L182
-                if app.statuscode == 0 and (app.keep_going or app.warningiserror):
-                    app.statuscode = 1
-
                 # get the text for used filter, either from filter string or function name
                 if callable(warning_filter):
                     warning_text = warning_filter.__name__
@@ -105,7 +96,7 @@ def process_warnings(app: Sphinx, exception: Exception | None) -> None:
                             ", ".join(need_ids),
                             warning_text,
                         ),
-                        None,
+                        "warnings",
                         None,
                     )
                 else:
@@ -123,6 +114,6 @@ def process_warnings(app: Sphinx, exception: Exception | None) -> None:
             log_warning(
                 logger,
                 "warnings were raised. See console / log output for details.",
-                None,
+                "warnings",
                 None,
             )


Test patch:
diff --git a/tests/test_needs_external_needs_build.py b/tests/test_needs_external_needs_build.py
index 11cdbc8c..c64bda2e 100644
--- a/tests/test_needs_external_needs_build.py
+++ b/tests/test_needs_external_needs_build.py
@@ -4,7 +4,6 @@ from pathlib import Path
 
 import pytest
 import responses
-import sphinx
 from docutils import __version__ as doc_ver
 
 
@@ -114,32 +113,29 @@ def test_external_needs_base_url_relative_path(test_app):
     assert root_table_hrefs[4].text == "EXT_TEST_01"
 
     # check needflow usage for base_url in root level
-    if not sphinx.__version__.startswith("3.5"):
-        if int(doc_ver.split(".")[1]) >= 18:
-            root_flow_hrefs = root_tree.xpath("//figure/p/object/a/img")
-            assert (
-                root_tree.xpath("//figure/figcaption/p/span/a")[0].text == "My needflow"
-            )
-        else:
-            root_flow_hrefs = root_tree.xpath(
-                "//div[@class='figure align-center']/p/object/a/img"
-            )
-            assert (
-                root_tree.xpath(
-                    "//div[@class='figure align-center']/p[@class='caption']/span/a"
-                )[0].text
-                == "My needflow"
-            )
-        # check base_url url in root level
-        assert (
-            "as EXT_TEST_01 [[http://my_company.com/docs/v1/index.html#TEST_01]]"
-            in root_flow_hrefs[0].attrib["alt"]
+    if int(doc_ver.split(".")[1]) >= 18:
+        root_flow_hrefs = root_tree.xpath("//figure/p/object/a/img")
+        assert root_tree.xpath("//figure/figcaption/p/span/a")[0].text == "My needflow"
+    else:
+        root_flow_hrefs = root_tree.xpath(
+            "//div[@class='figure align-center']/p/object/a/img"
         )
-        # check base_url relative path in root level
         assert (
-            "as EXT_REL_PATH_TEST_01 [[../../../_build/html/index.html#TEST_01]]"
-            in root_flow_hrefs[0].attrib["alt"]
+            root_tree.xpath(
+                "//div[@class='figure align-center']/p[@class='caption']/span/a"
+            )[0].text
+            == "My needflow"
         )
+    # check base_url url in root level
+    assert (
+        "as EXT_TEST_01 [[http://my_company.com/docs/v1/index.html#TEST_01]]"
+        in root_flow_hrefs[0].attrib["alt"]
+    )
+    # check base_url relative path in root level
+    assert (
+        "as EXT_REL_PATH_TEST_01 [[../../../_build/html/index.html#TEST_01]]"
+        in root_flow_hrefs[0].attrib["alt"]
+    )
 
     # check role need_outgoing and need_incoming for base_url in root level
     for element in root_tree.xpath("//p/span/a"):
@@ -190,32 +186,29 @@ def test_external_needs_base_url_relative_path(test_app):
     assert sub_table_hrefs[4].text == "EXT_TEST_01"
 
     # check needflow usage for base_url in subfolder level
-    if not sphinx.__version__.startswith("3.5"):
-        if int(doc_ver.split(".")[1]) >= 18:
-            sub_flow_hrefs = sub_tree.xpath("//figure/p/object/a/img")
-            assert (
-                sub_tree.xpath("//figure/figcaption/p/span/a")[0].text == "My needflow"
-            )
-        else:
-            sub_flow_hrefs = sub_tree.xpath(
-                "//div[@class='figure align-center']/p/object/a/img"
-            )
-            assert (
-                sub_tree.xpath(
-                    "//div[@class='figure align-center']/p[@class='caption']/span/a"
-                )[0].text
-                == "My needflow"
-            )
-        # check base_url url in root level
-        assert (
-            "as EXT_TEST_01 [[http://my_company.com/docs/v1/index.html#TEST_01]]"
-            in sub_flow_hrefs[0].attrib["alt"]
+    if int(doc_ver.split(".")[1]) >= 18:
+        sub_flow_hrefs = sub_tree.xpath("//figure/p/object/a/img")
+        assert sub_tree.xpath("//figure/figcaption/p/span/a")[0].text == "My needflow"
+    else:
+        sub_flow_hrefs = sub_tree.xpath(
+            "//div[@class='figure align-center']/p/object/a/img"
         )
-        # check base_url relative path in subfolder level, one level deeper than base_url
         assert (
-            "as EXT_REL_PATH_TEST_01 [[../../../_build/html/index.html#TEST_01]]"
-            in sub_flow_hrefs[0].attrib["alt"]
+            sub_tree.xpath(
+                "//div[@class='figure align-center']/p[@class='caption']/span/a"
+            )[0].text
+            == "My needflow"
         )
+    # check base_url url in root level
+    assert (
+        "as EXT_TEST_01 [[http://my_company.com/docs/v1/index.html#TEST_01]]"
+        in sub_flow_hrefs[0].attrib["alt"]
+    )
+    # check base_url relative path in subfolder level, one level deeper than base_url
+    assert (
+        "as EXT_REL_PATH_TEST_01 [[../../../_build/html/index.html#TEST_01]]"
+        in sub_flow_hrefs[0].attrib["alt"]
+    )
 
     # check role need_outgoing and need_incoming for base_url in subfolder level
     for element in sub_tree.xpath("//p/span/a"):
@@ -272,35 +265,33 @@ def test_external_needs_base_url_relative_path(test_app):
     assert sub_sub_table_hrefs[4].text == "EXT_TEST_01"
 
     # check needflow usage for base_url in subsubfolder level
-    if not sphinx.__version__.startswith("3.5"):
-        if int(doc_ver.split(".")[1]) >= 18:
-            sub_sub_flow_hrefs = sub_sub_tree.xpath("//figure/p/object/a/img")
-            assert (
-                sub_sub_tree.xpath("//figure/figcaption/p/span/a")[0].text
-                == "My needflow"
-            )
-        else:
-            sub_sub_flow_hrefs = sub_tree.xpath(
-                "//div[@class='figure align-center']/p/object/a/img"
-            )
-            assert (
-                sub_sub_tree.xpath(
-                    "//div[@class='figure align-center']/p[@class='caption']/span/a"
-                )[0].text
-                == "My needflow"
-            )
-
-        # check base_url url in subsubfolder level
+    if int(doc_ver.split(".")[1]) >= 18:
+        sub_sub_flow_hrefs = sub_sub_tree.xpath("//figure/p/object/a/img")
         assert (
-            "as EXT_TEST_01 [[http://my_company.com/docs/v1/index.html#TEST_01]]"
-            in sub_sub_flow_hrefs[0].attrib["alt"]
+            sub_sub_tree.xpath("//figure/figcaption/p/span/a")[0].text == "My needflow"
+        )
+    else:
+        sub_sub_flow_hrefs = sub_tree.xpath(
+            "//div[@class='figure align-center']/p/object/a/img"
         )
-        # check base_url relative path in subsubfolder level, two level deeper than base_url
         assert (
-            "as EXT_REL_PATH_TEST_01 [[../../../_build/html/index.html#TEST_01]]"
-            in sub_sub_flow_hrefs[0].attrib["alt"]
+            sub_sub_tree.xpath(
+                "//div[@class='figure align-center']/p[@class='caption']/span/a"
+            )[0].text
+            == "My needflow"
         )
 
+    # check base_url url in subsubfolder level
+    assert (
+        "as EXT_TEST_01 [[http://my_company.com/docs/v1/index.html#TEST_01]]"
+        in sub_sub_flow_hrefs[0].attrib["alt"]
+    )
+    # check base_url relative path in subsubfolder level, two level deeper than base_url
+    assert (
+        "as EXT_REL_PATH_TEST_01 [[../../../_build/html/index.html#TEST_01]]"
+        in sub_sub_flow_hrefs[0].attrib["alt"]
+    )
+
     # check role need_outgoing and need_incoming for base_url in subsubfolder level
     for element in sub_sub_tree.xpath("//p/span/a"):
         # check link for need_outgoing
diff --git a/tests/test_needs_filter_data.py b/tests/test_needs_filter_data.py
index bf1fee90..67978b44 100644
--- a/tests/test_needs_filter_data.py
+++ b/tests/test_needs_filter_data.py
@@ -14,11 +14,6 @@ def test_doc_needs_filter_data_html(test_app):
     app.build()
     index_html = Path(app.outdir, "index.html").read_text()
 
-    import sphinx
-
-    if sphinx.__version__.startswith("3.5"):
-        return
-
     # Check need_count works
     assert "The amount of needs that belong to current variants: 6" in index_html
 
diff --git a/tests/test_needs_warning.py b/tests/test_needs_warning.py
index 866d9bab..b805b57c 100644
--- a/tests/test_needs_warning.py
+++ b/tests/test_needs_warning.py
@@ -1,11 +1,20 @@
+import os
 from pathlib import Path
 
 import pytest
+from sphinx import version_info
+from sphinx.util.console import strip_colors
 
 
 @pytest.mark.parametrize(
     "test_app",
-    [{"buildername": "html", "srcdir": "doc_test/doc_needs_warnings"}],
+    [
+        {
+            "buildername": "html",
+            "srcdir": "doc_test/doc_needs_warnings",
+            "no_plantuml": True,
+        }
+    ],
     indirect=True,
 )
 def test_needs_warnings(test_app):
@@ -13,33 +22,33 @@ def test_needs_warnings(test_app):
     app.build()
 
     # stdout warnings
-    warning = app._warning
-    warnings = warning.getvalue()
-
-    # check multiple warning registration
-    assert "'invalid_status' in 'needs_warnings' is already registered." in warnings
-
-    # check warnings contents
-    assert "WARNING: invalid_status: failed" in warnings
-    assert "failed needs: 2 (SP_TOO_001, US_63252)" in warnings
-    assert (
-        "used filter: status not in ['open', 'closed', 'done', 'example_2', 'example_3']"
-        in warnings
-    )
-
-    # check needs warning from custom defined filter code
-    assert "failed needs: 1 (TC_001)" in warnings
-    assert "used filter: my_custom_warning_check" in warnings
-
-    # negative test to check needs warning if need passed the warnings-check
-    assert "TC_NEG_001" not in warnings
-
-    # Check for warning registered via config api
-    assert "WARNING: api_warning_filter: failed" in warnings
-    assert "WARNING: api_warning_func: failed" in warnings
-
-    # Check warnings not including external needs
-    assert "EXT_TEST_01" not in warnings
+    warnings = strip_colors(
+        app._warning.getvalue().replace(str(app.srcdir) + os.sep, "srcdir/")
+    ).splitlines()
+
+    expected = [
+        "WARNING: 'invalid_status' in 'needs_warnings' is already registered. [needs.config]",
+        "WARNING: api_warning_filter: failed",
+        "\t\tfailed needs: 1 (TC_002)",
+        "\t\tused filter: status == 'example_2' [needs.warnings]",
+        "WARNING: api_warning_func: failed",
+        "\t\tfailed needs: 1 (TC_003)",
+        "\t\tused filter: custom_warning_func [needs.warnings]",
+        "WARNING: invalid_status: failed",
+        "\t\tfailed needs: 2 (SP_TOO_001, US_63252)",
+        "\t\tused filter: status not in ['open', 'closed', 'done', 'example_2', 'example_3'] [needs.warnings]",
+        "WARNING: type_match: failed",
+        "\t\tfailed needs: 1 (TC_001)",
+        "\t\tused filter: my_custom_warning_check [needs.warnings]",
+    ]
+
+    if version_info >= (7, 3):
+        expected.insert(
+            1,
+            "WARNING: cannot cache unpickable configuration value: 'needs_warnings' (because it contains a function, class, or module object)",
+        )
+
+    assert warnings == expected
 
 
 @pytest.mark.parametrize(
diff --git a/tests/test_needtable.py b/tests/test_needtable.py
index cdbed229..699b9c9f 100644
--- a/tests/test_needtable.py
+++ b/tests/test_needtable.py
@@ -69,8 +69,6 @@ def test_doc_build_html(test_app):
     indirect=True,
 )
 def test_doc_needtable_options(test_app):
-    import sphinx
-
     app = test_app
     app.build()
     html = Path(app.outdir, "test_options.html").read_text()
@@ -78,16 +76,7 @@ def test_doc_needtable_options(test_app):
     assert 'id="needtable-test_options-0"' in html
     assert 'id="needtable-test_options-1"' in html
 
-    if sphinx.version_info[0] < 2:
-        column_order = """
-<tr class="row-odd"><th class="head">Incoming</th>
-<th class="head">ID</th>
-<th class="head">Tags</th>
-<th class="head">Status</th>
-<th class="head">Title</th>
-"""
-    else:
-        column_order = """
+    column_order = """
 <tr class="row-odd"><th class="head"><p>Incoming</p></th>
 <th class="head"><p>ID</p></th>
 <th class="head"><p>Tags</p></th>
@@ -110,15 +99,7 @@ def test_doc_needtable_options(test_app):
     )
     assert "Sphinx-Needs docs for needs-string-links" in html
 
-    if sphinx.version_info[0] < 2:
-        string_column_order = """
-<tr class="row-odd"><th class="head">ID</th>
-<th class="head">Title</th>
-<th class="head">Config</th>
-<th class="head">Github</th>
-"""
-    else:
-        string_column_order = """
+    string_column_order = """
 <tr class="row-odd"><th class="head"><p>ID</p></th>
 <th class="head"><p>Title</p></th>
 <th class="head"><p>Config</p></th>