Repository: sphinx-doc/sphinx

Issue:
sphinx.ext.apidoc does not include header in modules.rst
### Describe the bug

The new sphinx.ext.apidoc extension does not have an option equivalent to `--project` of the CLI which becomes "header" in the code. The main issue with this is that this option is used to give the title of the `modules.rst` file that is generated by default. Without it being set, the default `""`, leads to no page title in `modules.rst`. This means that the `modules.rst` page not only has no title, but the title can't be overwritten in an external toctree (ex. `My API <api/modules>`).

Part of the confusion comes from "header" or "--project" being documented as for the "--full" case only, but it isn't.

### How to Reproduce

index.rst:

```restructuredtext
My Docs
=======

.. toctree::
    :maxdepth: 1

    My API <api/modules>

```

conf.py:

```python
# sphinx.ext.apidoc was added to sphinx in 8.2.0
needs_sphinx = "8.2.0"

extensions = ["sphinx.ext.autodoc", "sphinx.ext.apidoc"]

# API docs
apidoc_modules = [
    {
        "path": "../../satpy",
        "destination": "api/",
    },
]
apidoc_separate_modules = True
apidoc_include_private = True
```

### Environment Information

```text
Platform:              linux; (Linux-6.9.3-76060903-generic-x86_64-with-glibc2.35)
Python version:        3.12.8 | packaged by conda-forge | (main, Dec  5 2024, 14:24:40) [GCC 13.3.0])
Python implementation: CPython
Sphinx version:        8.2.1
Docutils version:      0.21.2
Jinja2 version:        3.1.5
Pygments version:      2.18.0
```

### Sphinx extensions

```python
["sphinx.ext.autodoc", "sphinx.ext.apidoc"]
```

### Additional context

The header is passed here:

https://github.com/sphinx-doc/sphinx/blob/44346bb79c889383e8e0dac860b70f9483f6c293/sphinx/ext/apidoc/_generate.py#L193-L197

And used in the template here:

https://github.com/sphinx-doc/sphinx/blob/44346bb79c889383e8e0dac860b70f9483f6c293/sphinx/templates/apidoc/toc.rst.jinja#L1

But is defined in code as `--full` only along with the actual documentation for the apidoc cli tool:

https://github.com/sphinx-doc/sphinx/blob/44346bb79c889383e8e0dac860b70f9483f6c293/sphinx/ext/apidoc/_shared.py#L61-L69

Discussion:
djhoese: @AA-Turner Not sure how long you want an authors description to get. Feel free to shorten/reword.
djhoese: Why is ruff complaining about my string literal but not the others in the same test module? Ah it is only checking the diff. I can update if you'd like but I'd argue that the existing is more readable. I could also do a multiline string which ruff might like better.
AA-Turner: > it is only checking the diff.

It should check everything, not just the diff.
djhoese: Yep, of course. I saw `--diff` in the CI log and made assumptions based on my confusion on the differences between my code and the rest in the module. I've run ruff locally and it put the string on one line. Should be ready for review now.
djhoese: @AA-Turner Any chance this PR could be included in 8.2.2?
AA-Turner: I'm not sure if it's technically a bug?
djhoese: Without this change the `modules.rst` can't be used properly (see related issue example) and is not equivalent to the apidoc CLI tool. How else should this file be used?

Patch:
diff --git a/AUTHORS.rst b/AUTHORS.rst
index 9ecbd9d5a..ff92ab7ea 100644
--- a/AUTHORS.rst
+++ b/AUTHORS.rst
@@ -45,7 +45,7 @@ Contributors
 * Daniel Eades -- improved static typing
 * Daniel Hahler -- testing and CI improvements
 * Daniel Pizetta -- inheritance diagram improvements
-* Dave Hoese -- ``sphinx.ext.viewcode`` bug fix
+* Dave Hoese -- ``sphinx.ext.viewcode`` and ``sphinx.ext.apidoc`` bug fixes
 * Dave Kuhlman -- original LaTeX writer
 * Dimitri Papadopoulos Orfanos -- linting and spelling
 * Dmitry Shachnev -- modernisation and reproducibility
diff --git a/CHANGES.rst b/CHANGES.rst
index 50919fe90..887f33d68 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -18,6 +18,8 @@ Bugs fixed
 
 * #13392: Fix argument type for ``jieba.load_userdict()``.
 * #13402: Ensure inline ``<script>`` tags are written exactly once.
+* #13391: apidoc: Fix TOC file not having a title.
+  Patch by Dave Hoese.
 
 Testing
 -------
diff --git a/sphinx/ext/apidoc/_extension.py b/sphinx/ext/apidoc/_extension.py
index cb40cc4fd..c5a67528e 100644
--- a/sphinx/ext/apidoc/_extension.py
+++ b/sphinx/ext/apidoc/_extension.py
@@ -217,7 +217,6 @@ def _parse_module_options(
         dest_dir=dest_path,
         module_path=module_path,
         exclude_pattern=exclude_patterns,
-        automodule_options=automodule_options,
         max_depth=max_depth,
         quiet=True,
         follow_links=bool_options['follow_links'],
@@ -226,6 +225,8 @@ def _parse_module_options(
         no_headings=bool_options['no_headings'],
         module_first=bool_options['module_first'],
         implicit_namespaces=bool_options['implicit_namespaces'],
+        automodule_options=automodule_options,
+        header=module_path.name,
     )
 
 


Test patch:
diff --git a/tests/test_extensions/test_ext_apidoc.py b/tests/test_extensions/test_ext_apidoc.py
index 7a9d62194..a97423370 100644
--- a/tests/test_extensions/test_ext_apidoc.py
+++ b/tests/test_extensions/test_ext_apidoc.py
@@ -782,13 +782,14 @@ def test_sphinx_extension(app: SphinxTestApp) -> None:
     app.build()
     assert app.warning.getvalue() == ''
 
-    assert set((app.srcdir / 'generated').iterdir()) == {
-        app.srcdir / 'generated' / 'modules.rst',
-        app.srcdir / 'generated' / 'my_package.rst',
-    }
-    assert 'show-inheritance' not in (
-        app.srcdir / 'generated' / 'my_package.rst'
-    ).read_text(encoding='utf8')
+    toc_file = app.srcdir / 'generated' / 'modules.rst'
+    pkg_file = app.srcdir / 'generated' / 'my_package.rst'
+    assert set((app.srcdir / 'generated').iterdir()) == {toc_file, pkg_file}
+    modules_content = toc_file.read_text(encoding='utf8')
+    assert modules_content == (
+        'src\n===\n\n.. toctree::\n   :maxdepth: 3\n\n   my_package\n'
+    )
+    assert 'show-inheritance' not in pkg_file.read_text(encoding='utf8')
     assert (app.outdir / 'generated' / 'my_package.html').is_file()
 
     # test a re-build