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

Issue:
`typehints_defaults` does not work when missing python3 sytle type hints
Given function signature 
```python
def grid_archive_heatmap(archive, ax=None, ... )
```
and setting `typehints_defaults = "comma" ("braces", "braces-after")` . `sphinx-autodoc-typehints` does not correctly add a "default=" clause for the argument `ax` in the documentation.

This problem was encountered at https://github.com/icaros-usc/pyribs/pull/204.


Pull request:
Fixed default options not displaying for parameters without type hints.
Resolves #234

`format_default()` takes a new parameter `is_annotated` to better format the option.

`_inject_types_to_docstring()` now iterates through all signature parameters. Unchanged behaviour for parameters with type hints, but parameters without type hints now get their default value shown.

Also added tests with 4 functions trying out different variations with/without type hints, default values, and return types.

Works with both the "braces" and "commas" setting for `typehints_defaults`.

Before:
<img width="481" alt="image" src="https://user-images.githubusercontent.com/67759015/210858637-ba2bfbbb-4705-4f0f-8911-b836b4403779.png">

After:
<img width="478" alt="image" src="https://user-images.githubusercontent.com/67759015/210858674-3fb62601-9a3e-49c5-9006-c66919e6ca0f.png">


Patch:
diff --git a/src/sphinx_autodoc_typehints/__init__.py b/src/sphinx_autodoc_typehints/__init__.py
index ae679d0..54ff42f 100644
--- a/src/sphinx_autodoc_typehints/__init__.py
+++ b/src/sphinx_autodoc_typehints/__init__.py
@@ -475,19 +475,27 @@ def split_type_comment_args(comment: str) -> list[str | None]:
     return result
 
 
-def format_default(app: Sphinx, default: Any) -> str | None:
+def format_default(app: Sphinx, default: Any, is_annotated: bool) -> str | None:
     if default is inspect.Parameter.empty:
         return None
     formatted = repr(default).replace("\\", "\\\\")
-    if app.config.typehints_defaults.startswith("braces"):
-        return f" (default: ``{formatted}``)"
+
+    if is_annotated:
+        if app.config.typehints_defaults.startswith("braces"):
+            return f" (default: ``{formatted}``)"
+        else:  # other option is comma
+            return f", default: ``{formatted}``"
     else:
-        return f", default: ``{formatted}``"
+        if app.config.typehints_defaults == "braces-after":
+            return f" (default: ``{formatted}``)"
+        else:
+            return f"default: ``{formatted}``"
 
 
 def process_docstring(
     app: Sphinx, what: str, name: str, obj: Any, options: Options | None, lines: list[str]  # noqa: U100
 ) -> None:
+
     original_obj = obj
     obj = obj.fget if isinstance(obj, property) else obj
     if not callable(obj):
@@ -561,41 +569,45 @@ def _inject_types_to_docstring(
     name: str,
     lines: list[str],
 ) -> None:
-    for arg_name, annotation in type_hints.items():
-        if arg_name == "return":
-            continue  # this is handled separately later
-        if signature is None or arg_name not in signature.parameters:
-            default = inspect.Parameter.empty
-        else:
+
+    if signature is not None:
+        for arg_name in signature.parameters:
+            annotation = type_hints.get(arg_name, None)
+
             default = signature.parameters[arg_name].default
-        if arg_name.endswith("_"):
-            arg_name = f"{arg_name[:-1]}\\_"
 
-        formatted_annotation = format_annotation(annotation, app.config)
+            if arg_name.endswith("_"):
+                arg_name = f"{arg_name[:-1]}\\_"
 
-        insert_index = None
-        for at, line in enumerate(lines):
-            if _line_is_param_line_for_arg(line, arg_name):
-                # Get the arg_name from the doc to match up for type in case it has a star prefix.
-                # Line is in the correct format so this is guaranteed to return tuple[str, str].
-                _, arg_name = _get_sphinx_line_keyword_and_argument(line)  # type: ignore[assignment, misc]
-                insert_index = at
-                break
+            insert_index = None
+            for at, line in enumerate(lines):
+                if _line_is_param_line_for_arg(line, arg_name):
+                    # Get the arg_name from the doc to match up for type in case it has a star prefix.
+                    # Line is in the correct format so this is guaranteed to return tuple[str, str].
+                    _, arg_name = _get_sphinx_line_keyword_and_argument(line)  # type: ignore[assignment, misc]
+                    insert_index = at
+                    break
+
+            if annotation is not None and insert_index is None and app.config.always_document_param_types:
+                lines.append(f":param {arg_name}:")
+                insert_index = len(lines)
 
-        if insert_index is None and app.config.always_document_param_types:
-            lines.append(f":param {arg_name}:")
-            insert_index = len(lines)
-
-        if insert_index is not None:
-            type_annotation = f":type {arg_name}: {formatted_annotation}"
-            if app.config.typehints_defaults:
-                formatted_default = format_default(app, default)
-                if formatted_default:
-                    if app.config.typehints_defaults.endswith("after"):
-                        lines[insert_index] += formatted_default
-                    else:  # add to last param doc line
-                        type_annotation += formatted_default
-            lines.insert(insert_index, type_annotation)
+            if insert_index is not None:
+                if annotation is None:
+                    type_annotation = f":type {arg_name}: "
+                else:
+                    formatted_annotation = format_annotation(annotation, app.config)
+                    type_annotation = f":type {arg_name}: {formatted_annotation}"
+
+                if app.config.typehints_defaults:
+                    formatted_default = format_default(app, default, annotation is not None)
+                    if formatted_default:
+                        if app.config.typehints_defaults.endswith("after"):
+                            lines[insert_index] += formatted_default
+                        else:  # add to last param doc line
+                            type_annotation += formatted_default
+
+                lines.insert(insert_index, type_annotation)
 
     if "return" in type_hints and not inspect.isclass(original_obj):
         if what == "method" and name.endswith(".__init__"):  # avoid adding a return type for data class __init__


Test patch:
diff --git a/tests/roots/test-dummy/dummy_module_without_complete_typehints.py b/tests/roots/test-dummy/dummy_module_without_complete_typehints.py
new file mode 100644
index 0000000..6d7e91e
--- /dev/null
+++ b/tests/roots/test-dummy/dummy_module_without_complete_typehints.py
@@ -0,0 +1,34 @@
+def function_with_some_defaults_and_without_typehints(x, y=None):  # noqa: U100
+    """
+    Function docstring.
+
+    :param x: foo
+    :param y: bar
+    """
+
+
+def function_with_some_defaults_and_some_typehints(x: int, y=None):  # noqa: U100
+    """
+    Function docstring.
+
+    :param x: foo
+    :param y: bar
+    """
+
+
+def function_with_some_defaults_and_more_typehints(x: int, y=None) -> str:  # noqa: U100
+    """
+    Function docstring.
+
+    :param x: foo
+    :param y: bar
+    """
+
+
+def function_with_defaults_and_some_typehints(x: int = 0, y=None) -> str:  # noqa: U100
+    """
+    Function docstring.
+
+    :param x: foo
+    :param y: bar
+    """
diff --git a/tests/roots/test-dummy/without_complete_typehints.rst b/tests/roots/test-dummy/without_complete_typehints.rst
new file mode 100644
index 0000000..2e7a204
--- /dev/null
+++ b/tests/roots/test-dummy/without_complete_typehints.rst
@@ -0,0 +1,7 @@
+Simple Module
+=============
+
+.. autofunction:: dummy_module_without_complete_typehints.function_with_some_defaults_and_without_typehints
+.. autofunction:: dummy_module_without_complete_typehints.function_with_some_defaults_and_some_typehints
+.. autofunction:: dummy_module_without_complete_typehints.function_with_some_defaults_and_more_typehints
+.. autofunction:: dummy_module_without_complete_typehints.function_with_defaults_and_some_typehints
diff --git a/tests/test_sphinx_autodoc_typehints.py b/tests/test_sphinx_autodoc_typehints.py
index 8fd463b..adcd40e 100644
--- a/tests/test_sphinx_autodoc_typehints.py
+++ b/tests/test_sphinx_autodoc_typehints.py
@@ -1121,3 +1121,62 @@ def test_sphinx_output_with_use_signature_and_return(app: SphinxTestApp, status:
           "str"
     """
     assert text_contents == dedent(expected_contents)
+
+
+@pytest.mark.sphinx("text", testroot="dummy")
+@patch("sphinx.writers.text.MAXWIDTH", 2000)
+def test_default_annotation_without_typehints(app: SphinxTestApp, status: StringIO) -> None:
+    set_python_path()
+    app.config.master_doc = "without_complete_typehints"  # type: ignore # create flag
+    app.config.typehints_defaults = "comma"  # type: ignore
+    app.build()
+    assert "build succeeded" in status.getvalue()
+    text_path = pathlib.Path(app.srcdir) / "_build" / "text" / "without_complete_typehints.txt"
+    text_contents = text_path.read_text().replace("–", "--")
+    expected_contents = """\
+    Simple Module
+    *************
+
+    dummy_module_without_complete_typehints.function_with_some_defaults_and_without_typehints(x, y=None)
+
+       Function docstring.
+
+       Parameters:
+          * **x** -- foo
+
+          * **y** (default: "None") -- bar
+
+    dummy_module_without_complete_typehints.function_with_some_defaults_and_some_typehints(x, y=None)
+
+       Function docstring.
+
+       Parameters:
+          * **x** ("int") -- foo
+
+          * **y** (default: "None") -- bar
+
+    dummy_module_without_complete_typehints.function_with_some_defaults_and_more_typehints(x, y=None)
+
+       Function docstring.
+
+       Parameters:
+          * **x** ("int") -- foo
+
+          * **y** (default: "None") -- bar
+
+       Return type:
+          "str"
+
+    dummy_module_without_complete_typehints.function_with_defaults_and_some_typehints(x=0, y=None)
+
+       Function docstring.
+
+       Parameters:
+          * **x** ("int", default: "0") -- foo
+
+          * **y** (default: "None") -- bar
+
+       Return type:
+          "str"
+    """
+    assert text_contents == dedent(expected_contents)