Repository: tox-dev/sphinx-autodoc-typehints (python)

Issue:
Sphinx option rst_prolog and rst_epilog break return type injection
When adding any of those sphinx options: [rst_epilog](https://www.sphinx-doc.org/en/master/usage/configuration.html#confval-rst_epilog) or [rst_prolog](https://www.sphinx-doc.org/en/master/usage/configuration.html#confval-rst_prolog)

It will break the return type injection. For example we have one for the minimum python version (`min_python_version`). 

When using the `rst_epilog` all of the return types become the version: 
![image](https://github.com/tox-dev/sphinx-autodoc-typehints/assets/17804711/2019e67b-4e22-4694-83a4-9d5726268034)

If you use the `rst_prolog` the return types are working again, but now the return type always appear at the top of the class, before the header and parameters.

![image](https://github.com/tox-dev/sphinx-autodoc-typehints/assets/17804711/593b1c23-2f74-41c8-abe1-8fc82d4f4206)

Disabling either will produce the correct result:
![image](https://github.com/tox-dev/sphinx-autodoc-typehints/assets/17804711/0e0ea99d-a67d-4100-85c9-9e251757a301)

Due to the rst, it creates a bunch of nodes either before or after: `<substitution_definition names="min_python_version">3.8</substitution_definition>` which mess with the line parsing.  Since it's assuming certain things come after paramters, it doesn't pick the correct lines.


Pull request description:
Return injection fix
Resolves #444 and #425

Ignore the rst_epilog/rst_prolog during parsing type hints like #436 did. This still keeps the replacement behavior.

I tried adjusting the parsing, but the XML node line counting, and the list of lines given are too intertwined without rewriting it completely.

`test_sphinx_autodoc_typehints` was broken for me in Windows, so I have added an encoding kwarg for it to work.

EDIT: Working on making tests pass... and adding them.

Patch:
diff --git a/README.md b/README.md
index c941e93..ff2ff49 100644
--- a/README.md
+++ b/README.md
@@ -62,6 +62,9 @@ The following configuration options are accepted:
   `module.for.Class`). If `False`, just the class name displays (e.g. `Class`)
 - `always_document_param_types` (default: `False`): If `False`, do not add type info for undocumented parameters. If
   `True`, add stub documentation for undocumented parameters to be able to add type info.
+- `always_use_bars_union ` (default: `False`): If `True`, display Union's using the | operator described in PEP 604.
+   (e.g `X` | `Y` or `int` | `None`). If `False`, Unions will display with the typing in brackets. (e.g. `Union[X, Y]`
+   or `Optional[int]`)
 - `typehints_document_rtype` (default: `True`): If `False`, never add an `:rtype:` directive. If `True`, add the
   `:rtype:` directive if no existing `:rtype:` is found.
 - `typehints_use_rtype` (default: `True`): Controls behavior when `typehints_document_rtype` is set to `True`. If
diff --git a/src/sphinx_autodoc_typehints/__init__.py b/src/sphinx_autodoc_typehints/__init__.py
index 8c814d1..4961fd8 100644
--- a/src/sphinx_autodoc_typehints/__init__.py
+++ b/src/sphinx_autodoc_typehints/__init__.py
@@ -779,6 +779,9 @@ def node_line_no(node: Node) -> int | None:
     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.
     """
+    if node is None:
+        return None
+
     while node.line is None and node.children:
         node = node.children[0]
     return node.line
diff --git a/src/sphinx_autodoc_typehints/parser.py b/src/sphinx_autodoc_typehints/parser.py
index ea38444..1f93881 100644
--- a/src/sphinx_autodoc_typehints/parser.py
+++ b/src/sphinx_autodoc_typehints/parser.py
@@ -13,13 +13,20 @@ if TYPE_CHECKING:
 
     from docutils import nodes
     from docutils.frontend import Values
+    from docutils.statemachine import StringList
+
+
+class _RstSnippetParser(RSTParser):
+    @staticmethod
+    def decorate(_content: StringList) -> None:
+        """Override to skip processing rst_epilog/rst_prolog for typing."""
 
 
 def parse(inputstr: str, settings: Values | optparse.Values) -> nodes.document:
     """Parse inputstr and return a docutils document."""
     doc = new_document("", settings=settings)
     with sphinx_domains(settings.env):
-        parser = RSTParser()
+        parser = _RstSnippetParser()
         parser.set_application(settings.env.app)
         parser.parse(inputstr, doc)
     return doc


Test patch:
diff --git a/tests/test_integration.py b/tests/test_integration.py
index 773f884..8d732d9 100644
--- a/tests/test_integration.py
+++ b/tests/test_integration.py
@@ -1268,14 +1268,100 @@ def typehints_use_signature(a: AsyncGenerator) -> AsyncGenerator:
     return a
 
 
+prolog = """
+.. |test_node_start| replace:: {test_node_start}
+""".format(test_node_start="test_start")
+
+
+@expected(
+    """
+    mod.docstring_with_multiline_note_after_params_prolog_replace(param)
+
+       Do something.
+
+       Parameters:
+          **param** ("int") -- A parameter.
+
+       Return type:
+          "None"
+
+       Note:
+
+         Some notes. test_start More notes
+
+    """,
+    rst_prolog=prolog,
+)
+def docstring_with_multiline_note_after_params_prolog_replace(param: int) -> None:  # noqa: ARG001
+    """Do something.
+
+    Args:
+        param: A parameter.
+
+    Note:
+
+        Some notes. |test_node_start|
+        More notes
+    """
+
+
+epilog = """
+.. |test_node_end| replace:: {test_node_end}
+""".format(test_node_end="test_end")
+
+
+@expected(
+    """
+    mod.docstring_with_multiline_note_after_params_epilog_replace(param)
+
+       Do something.
+
+       Parameters:
+          **param** ("int") -- A parameter.
+
+       Return type:
+          "None"
+
+       Note:
+
+         Some notes. test_end More notes
+
+    """,
+    rst_epilog=epilog,
+)
+def docstring_with_multiline_note_after_params_epilog_replace(param: int) -> None:  # noqa: ARG001
+    """Do something.
+
+    Args:
+        param: A parameter.
+
+    Note:
+
+        Some notes. |test_node_end|
+        More notes
+    """
+
+
+# Config settings for each test run.
+# Config Name: Sphinx Options as Dict.
+configs = {
+    "default_conf": {},
+    "prolog_conf": {"rst_prolog": prolog},
+    "epilog_conf": {
+        "rst_epilog": epilog,
+    },
+    "bothlog_conf": {
+        "rst_prolog": prolog,
+        "rst_epilog": epilog,
+    },
+}
+
+
 @pytest.mark.parametrize("val", [x for x in globals().values() if hasattr(x, "EXPECTED")])
+@pytest.mark.parametrize("conf_run", ["default_conf", "prolog_conf", "epilog_conf", "bothlog_conf"])
 @pytest.mark.sphinx("text", testroot="integration")
 def test_integration(
-    app: SphinxTestApp,
-    status: StringIO,
-    warning: StringIO,
-    monkeypatch: pytest.MonkeyPatch,
-    val: Any,
+    app: SphinxTestApp, status: StringIO, warning: StringIO, monkeypatch: pytest.MonkeyPatch, val: Any, conf_run: str
 ) -> None:
     if isclass(val) and issubclass(val, BaseException):
         template = AUTO_EXCEPTION
@@ -1285,6 +1371,7 @@ def test_integration(
         template = AUTO_FUNCTION
 
     (Path(app.srcdir) / "index.rst").write_text(template.format(val.__name__))
+    app.config.__dict__.update(configs[conf_run])
     app.config.__dict__.update(val.OPTIONS)
     monkeypatch.setitem(sys.modules, "mod", sys.modules[__name__])
     app.build()
diff --git a/tests/test_sphinx_autodoc_typehints.py b/tests/test_sphinx_autodoc_typehints.py
index 6cef7ca..3c706e5 100644
--- a/tests/test_sphinx_autodoc_typehints.py
+++ b/tests/test_sphinx_autodoc_typehints.py
@@ -607,7 +607,9 @@ def test_sphinx_output_default_role(app: SphinxTestApp, status: StringIO) -> Non
 
     assert "build succeeded" in status.getvalue()  # Build succeeded
 
-    contents_lines = (Path(app.srcdir) / "_build/pseudoxml/simple_default_role.pseudoxml").read_text().splitlines()
+    contents_lines = (
+        (Path(app.srcdir) / "_build/pseudoxml/simple_default_role.pseudoxml").read_text(encoding="utf-8").splitlines()
+    )
     list_item_idxs = [i for i, line in enumerate(contents_lines) if line.strip() == "<list_item>"]
     foo_param = dedent("\n".join(contents_lines[list_item_idxs[0] : list_item_idxs[1]]))
     expected_foo_param = """\