├── .editorconfig ├── .github └── workflows │ └── ci.yaml ├── .gitignore ├── .pre-commit-config.yaml ├── CONTRIBUTING.rst ├── LICENSE ├── README.rst ├── pyproject.toml ├── requirements.txt ├── setup.cfg ├── setup.py ├── sphinxcontrib ├── __init__.py └── apidoc │ ├── __init__.py │ └── ext.py ├── test-requirements.txt ├── tests ├── conftest.py ├── roots │ ├── test-advanced-negative │ │ ├── apidoc_dummy_module.py │ │ ├── apidoc_dummy_package │ │ │ ├── __init__.py │ │ │ ├── apidoc_dummy_submodule_a.py │ │ │ └── apidoc_dummy_submodule_b.py │ │ ├── conf.py │ │ └── index.rst │ ├── test-advanced │ │ ├── apidoc_dummy_module.py │ │ ├── apidoc_dummy_package │ │ │ ├── __init__.py │ │ │ ├── _apidoc_private_dummy_submodule.py │ │ │ ├── apidoc_dummy_submodule_a.py │ │ │ └── apidoc_dummy_submodule_b.py │ │ ├── conf.py │ │ └── index.rst │ ├── test-basics │ │ ├── apidoc_dummy_module.py │ │ ├── conf.py │ │ └── index.rst │ ├── test-invalid-directory │ │ ├── apidoc_dummy_module.py │ │ ├── conf.py │ │ └── index.rst │ └── test-missing-configuration │ │ ├── apidoc_dummy_module.py │ │ ├── conf.py │ │ └── index.rst └── test_ext.py └── tox.ini /.editorconfig: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | root = true 4 | 5 | [*] 6 | indent_style = space 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | charset = utf-8 11 | 12 | # Python files 13 | [*.py] 14 | indent_size = 4 15 | # isort plugin configuration 16 | known_first_party = sphinxcontrib 17 | multi_line_output = 2 18 | default_section = THIRDPARTY 19 | skip = .eggs docs 20 | 21 | # .travis.yml 22 | [.travis.yml] 23 | indent_size = 2 24 | 25 | # Dockerfile 26 | [Dockerfile] 27 | indent_size = 4 28 | 29 | # Makefile 30 | [Makefile] 31 | indent_size = 4 32 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: CI 3 | on: 4 | - push 5 | - pull_request 6 | jobs: 7 | lint: 8 | name: Run linters 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Checkout source code 12 | uses: actions/checkout@v3 13 | - name: Set up Python 3.13 14 | uses: actions/setup-python@v4 15 | with: 16 | python-version: "3.13" 17 | - name: Install dependencies 18 | run: python -m pip install tox 19 | - name: Run tox 20 | run: tox -e style 21 | test: 22 | name: Run unit tests 23 | runs-on: ubuntu-latest 24 | strategy: 25 | matrix: 26 | python: ["3.9", "3.10", "3.11", "3.12", "3.13"] 27 | steps: 28 | - name: Checkout source code 29 | uses: actions/checkout@v3 30 | # We need history to build the package 31 | with: 32 | fetch-depth: 0 33 | - name: Set up Python ${{ matrix.python }} 34 | uses: actions/setup-python@v4 35 | with: 36 | python-version: ${{ matrix.python }} 37 | - name: Install dependencies 38 | run: python -m pip install tox 39 | - name: Run unit tests (via tox) 40 | # Run tox using the version of Python in `PATH` 41 | run: tox -e py 42 | release: 43 | name: Upload release artifacts 44 | runs-on: ubuntu-latest 45 | permissions: 46 | id-token: write 47 | needs: test 48 | if: github.event_name == 'push' 49 | steps: 50 | - name: Checkout source code 51 | uses: actions/checkout@v3 52 | # We need history to build the package 53 | with: 54 | fetch-depth: 0 55 | - name: Set up Python 3.13 56 | uses: actions/setup-python@v4 57 | with: 58 | python-version: "3.13" 59 | - name: Install dependencies 60 | run: python -m pip install build 61 | - name: Build a binary wheel and a source tarball 62 | run: python -m build --sdist --wheel --outdir dist/ . 63 | - name: Publish distribution to PyPI 64 | if: startsWith(github.ref, 'refs/tags') 65 | uses: pypa/gh-action-pypi-publish@release/v1 66 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | 3 | # C extensions 4 | *.so 5 | 6 | # Packages 7 | *.egg* 8 | *.egg-info 9 | dist 10 | build 11 | eggs 12 | parts 13 | bin 14 | var 15 | sdist 16 | develop-eggs 17 | .installed.cfg 18 | lib 19 | lib64 20 | 21 | # Installer logs 22 | pip-log.txt 23 | 24 | # Unit test / coverage reports 25 | cover/ 26 | .coverage* 27 | .tox 28 | .venv 29 | .pytest_cache/ 30 | .mypy_cache/ 31 | 32 | # Translations 33 | *.mo 34 | 35 | # Complexity 36 | output/*.html 37 | output/*/index.html 38 | 39 | # Sphinx 40 | doc/build 41 | 42 | # pbr 43 | AUTHORS 44 | ChangeLog 45 | 46 | # Editors 47 | *~ 48 | .*.swp 49 | .*sw? 50 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | # See https://pre-commit.com for more information 2 | # See https://pre-commit.com/hooks.html for more hooks 3 | --- 4 | repos: 5 | - repo: https://github.com/pre-commit/pre-commit-hooks 6 | rev: v5.0.0 7 | hooks: 8 | - id: trailing-whitespace 9 | - id: mixed-line-ending 10 | args: ['--fix', 'lf'] 11 | - id: check-byte-order-marker 12 | - id: check-executables-have-shebangs 13 | - id: check-merge-conflict 14 | - id: debug-statements 15 | - id: end-of-file-fixer 16 | - id: check-yaml 17 | files: .*\.(yaml|yml)$ 18 | - id: check-added-large-files 19 | - repo: https://github.com/astral-sh/ruff-pre-commit 20 | rev: v0.11.8 21 | hooks: 22 | - id: ruff 23 | args: ['--fix', '--unsafe-fixes'] 24 | - id: ruff-format 25 | - repo: https://github.com/pre-commit/mirrors-mypy 26 | rev: v1.15.0 27 | hooks: 28 | - id: mypy 29 | additional_dependencies: 30 | - types-docutils 31 | args: ['--explicit-package-bases'] 32 | exclude: | 33 | (?x)( 34 | tests/.* 35 | ) 36 | -------------------------------------------------------------------------------- /CONTRIBUTING.rst: -------------------------------------------------------------------------------- 1 | ============== 2 | Contributing 3 | ============== 4 | 5 | If you would like to contribute to this project, or any project in the 6 | `sphinx-contrib`_ namespace, you should refer to the guidelines found in the 7 | `Sphinx documentation`_. 8 | 9 | .. _sphinx-contrib: https://github.com/sphinx-contrib 10 | .. _Sphinx documentation: http://www.sphinx-doc.org/en/stable/contrib/ 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2018 by Stephen Finucane 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are 6 | met: 7 | 8 | * Redistributions of source code must retain the above copyright 9 | notice, this list of conditions and the following disclaimer. 10 | 11 | * Redistributions in binary form must reproduce the above copyright 12 | notice, this list of conditions and the following disclaimer in the 13 | documentation and/or other materials provided with the distribution. 14 | 15 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 16 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 17 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 18 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 19 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 20 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 21 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 22 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 23 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 24 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ==================== 2 | sphinxcontrib-apidoc 3 | ==================== 4 | 5 | .. image:: https://github.com/sphinx-contrib/apidoc/actions/workflows/ci.yaml/badge.svg 6 | :target: https://github.com/sphinx-contrib/apidoc/actions/workflows/ci.yaml 7 | :alt: Build Status 8 | 9 | A Sphinx extension for running `sphinx-apidoc`_ on each build. 10 | 11 | .. important:: 12 | 13 | Sphinx 8.2.0 introduced a built-in extension adding this functionality, 14 | with many of the same options. New users should opt for this extension 15 | instead, while existing users should seek to migrate as this project will 16 | eventually be retired. For more information, refer to the 17 | `sphinx.ext.apidoc`_ documentation. A migration guide is provided `below 18 | `_ extension, documents a whole package in 25 | the style of other automatic API documentation tools. *sphinx-apidoc* does not 26 | actually build documentation - rather it simply generates it. As a result, it 27 | must be run before *sphinx-build*. This generally results in ``tox.ini`` files 28 | like the following: 29 | 30 | .. code-block:: ini 31 | 32 | [testenv:docs] 33 | commands = 34 | sphinx-apidoc -o doc/api my_code my_code/tests 35 | sphinx-build -W -b html doc doc/_build/html 36 | 37 | This extension eliminates the need to keep that configuration outside Sphinx. 38 | Instead, this functionality can be enabled and configured from your 39 | documentation's ``conf.py`` file, like so: 40 | 41 | .. code-block:: python 42 | 43 | extensions = [ 44 | 'sphinxcontrib.apidoc', 45 | # ... 46 | ] 47 | apidoc_module_dir = '../my_code' 48 | apidoc_output_dir = 'reference' 49 | apidoc_excluded_paths = ['tests'] 50 | apidoc_separate_modules = True 51 | 52 | Configuration 53 | ------------- 54 | 55 | The *apidoc* extension uses the following configuration values: 56 | 57 | ``apidoc_module_dir`` 58 | The path to the module to document. This must be a path to a Python package. 59 | This path can be a path relative to the documentation source directory or an 60 | absolute path. 61 | 62 | **Required** 63 | 64 | ``apidoc_output_dir`` 65 | The output directory. If it does not exist, it is created. This path is 66 | relative to the documentation source directory. 67 | 68 | **Optional**, defaults to ``api``. 69 | 70 | ``apidoc_template_dir`` 71 | A directory containing user-defined templates. Template files in this 72 | directory that match the default apidoc templates (``module.rst_t``, 73 | ``package.rst_t``, ``toc.rst_t``) will overwrite them. The default templates 74 | can be found in ``site-packages/sphinx/templates/apidoc/``. This path is 75 | relative to the documentation source directory. 76 | 77 | **Optional**, defaults to ``'templates'``. 78 | 79 | ``apidoc_excluded_paths`` 80 | An optional list of modules to exclude. These should be paths relative to 81 | ``apidoc_module_dir``. fnmatch-style wildcarding is supported. 82 | 83 | **Optional**, defaults to ``[]``. 84 | 85 | ``apidoc_separate_modules`` 86 | Put documentation for each module on its own page. Otherwise there will be 87 | one page per (sub)package. 88 | 89 | **Optional**, defaults to ``False``. 90 | 91 | ``apidoc_toc_file`` 92 | Filename for a table of contents file. Defaults to ``modules``. If set to 93 | ``False``, *apidoc* will not create a table of contents file. 94 | 95 | **Optional**, defaults to ``None``. 96 | 97 | ``apidoc_module_first`` 98 | When set to ``True``, put module documentation before submodule 99 | documentation. 100 | 101 | **Optional**, defaults to ``False``. 102 | 103 | ``apidoc_extra_args`` 104 | Extra arguments which will be passed to ``sphinx-apidoc``. These are placed 105 | after flags and before the module name. 106 | 107 | **Optional**, defaults to ``[]``. 108 | 109 | .. _migration-to-sphinx.ext.apidoc: 110 | 111 | Migration to ``sphinx.ext.apidoc`` 112 | ---------------------------------- 113 | 114 | Starting in Sphinx 8.2.0, Sphinx includes the `sphinx.ext.apidoc`_ extension, 115 | which includes most or all of the functionality provided by this extension. It 116 | should be preferred for both new and existing users. Consider the previous 117 | ``conf.py`` example given above: 118 | 119 | .. code-block:: python 120 | 121 | extensions = [ 122 | 'sphinxcontrib.apidoc', 123 | # ... 124 | ] 125 | apidoc_module_dir = '../my_code' 126 | apidoc_output_dir = 'reference' 127 | apidoc_excluded_paths = ['tests'] 128 | apidoc_separate_modules = True 129 | 130 | This can be rewritten to use the new extension like so: 131 | 132 | .. code-block:: python 133 | 134 | extensions = [ 135 | 'sphinx.ext.apidoc', 136 | # ... 137 | ] 138 | apidoc_modules = [ 139 | { 140 | 'path': '../my_code', 141 | 'destination': 'reference', 142 | 'exclude_patterns': ['**/tests/*'], 143 | 'separate_modules': True, 144 | }, 145 | ] 146 | 147 | For more information, refer to the `sphinx.ext.apidoc`_ documentation. 148 | 149 | Migration from pbr 150 | ------------------ 151 | 152 | `pbr`_ has historically included a custom variant of the `build_sphinx`_ 153 | distutils command. This provides, among other things, the ability to generate 154 | API documentation as part of build process. Clearly this is not necessary with 155 | this extension. 156 | 157 | There are two implementations of the API documentation feature in *pbr*: 158 | *autodoc_tree* and *autodoc*. To describe the difference, let's explore how one 159 | would migrate real-world projects using both features. Your project might use 160 | one or both: *autodoc_tree* is enabled using the ``autodoc_tree_index_modules`` 161 | setting while *autodoc* is enabled using the ``autodoc_index_modules`` 162 | setting, both found in the ``[pbr]`` section of ``setup.cfg``. 163 | 164 | autodoc_tree 165 | ~~~~~~~~~~~~ 166 | 167 | As *autodoc_tree* is based on *sphinx-apidoc*, migration is easy. Lets take 168 | `python-openstackclient`_ as an example, looking at minimal versions of 169 | ``setup.cfg`` and ``doc/source/conf.py``: 170 | 171 | .. code-block:: ini 172 | 173 | [build_sphinx] 174 | all_files = 1 175 | build-dir = doc/build 176 | source-dir = doc/source 177 | 178 | [pbr] 179 | autodoc_tree_index_modules = True 180 | autodoc_tree_excludes = 181 | setup.py 182 | openstackclient/volume/v3 183 | openstackclient/tests/ 184 | openstackclient/tests/* 185 | api_doc_dir = contributor/api 186 | 187 | .. code-block:: python 188 | 189 | extensions = [''] 190 | 191 | Once migrated, this would look like so: 192 | 193 | .. code-block:: ini 194 | 195 | [build_sphinx] 196 | all_files = 1 197 | build-dir = doc/build 198 | source-dir = doc/source 199 | 200 | .. code-block:: python 201 | 202 | extensions = ['sphinxcontrib.apidoc'] 203 | 204 | apidoc_module_dir = '../../openstack' 205 | apidoc_excluded_paths = [ 206 | 'volume', 207 | 'tests' 208 | ] 209 | apidoc_output_dir = 'contributor/api' 210 | 211 | There are a couple of changes here: 212 | 213 | #. Configure ``apidoc_module_dir`` in ``conf.py`` 214 | 215 | With the *autodoc_tree* feature, API documentation is always generated for 216 | the directory in which ``setup.cfg`` exists, which is typically the 217 | top-level directory. With this extension, you must explicitly state which 218 | directory you wish to build documentation for using the 219 | ``apidoc_module_dir`` setting. You should configure this to point to your 220 | actual package rather than the top level directory as this means you don't 221 | need to worry about skipping unrelated files like ``setup.py``. 222 | 223 | #. Configure ``apidoc_excluded_paths`` in ``conf.py`` 224 | 225 | The ``apidoc_excluded_paths`` setting in ``conf.py`` works exactly like the 226 | ``[pbr] autodoc_tree_excludes`` setting in ``setup.cfg``; namely, it's a 227 | list of fnmatch-style paths describing files and directories to exclude 228 | relative to the source directory. This means you can use the values from the 229 | ``[pbr] autodoc_tree_excludes`` setting, though you may need to update 230 | these if you configured ``apidoc_module_dir`` to point to something other 231 | than the top-level directory. 232 | 233 | #. Configure ``apidoc_output_dir`` in ``conf.py`` 234 | 235 | The ``apidoc_output_dir`` setting in ``conf.py`` works exactly like the 236 | ``[pbr] api_doc_dir`` setting in ``setup.cfg``; namely, it's a path relative 237 | to the documentation source directory to which all API documentation should 238 | be written. You can just copy the value from the ``[pbr] api_doc_dir`` 239 | setting. 240 | 241 | #. Remove settings from ``setup.cfg`` 242 | 243 | Remove the following settings from the ``[pbr]`` section of the 244 | ``setup.cfg`` file: 245 | 246 | - ``autodoc_tree_index_modules`` 247 | - ``autodoc_tree_excludes`` 248 | - ``api_doc_dir`` 249 | 250 | You may also wish to remove the entirety of the ``[build_sphinx]`` section, 251 | should you wish to build docs using ``sphinx-build`` instead. 252 | 253 | Once done, your output should work exactly as before. 254 | 255 | autodoc 256 | ~~~~~~~ 257 | 258 | *autodoc* is not based on *sphinx-apidoc*. Fortunately it is possible to 259 | generate something very similar (although not identical!). Let's take 260 | `oslo.privsep`_ as an example, once again looking at minimal versions of 261 | ``setup.cfg`` and ``doc/source/conf.py``: 262 | 263 | .. code-block:: ini 264 | 265 | [build_sphinx] 266 | all_files = 1 267 | build-dir = doc/build 268 | source-dir = doc/source 269 | 270 | [pbr] 271 | autodoc_index_modules = True 272 | api_doc_dir = reference/api 273 | autodoc_exclude_modules = 274 | oslo_privsep.tests.* 275 | oslo_privsep._* 276 | 277 | .. code-block:: python 278 | 279 | extensions = [''] 280 | 281 | Once migrated, this would look like so: 282 | 283 | .. code-block:: ini 284 | 285 | [build_sphinx] 286 | all_files = 1 287 | build-dir = doc/build 288 | source-dir = doc/source 289 | 290 | .. code-block:: python 291 | 292 | extensions = ['sphinxcontrib.apidoc'] 293 | 294 | apidoc_module_dir = '../../oslo_privsep' 295 | apidoc_excluded_paths = ['tests', '_*'] 296 | apidoc_output_dir = 'reference/api' 297 | apidoc_separate_modules = True 298 | 299 | Most of the changes necessary are the same as `autodoc_tree`_, with some 300 | exceptions. 301 | 302 | #. Configure ``apidoc_module_dir`` in ``conf.py`` 303 | 304 | With the *autodoc* feature, API documentation is always generated for 305 | the directory in which ``setup.cfg`` exists, which is typically the 306 | top-level directory. With this extension, you must explicitly state which 307 | directory you wish to build documentation for using the 308 | ``apidoc_module_dir`` setting. You should configure this to point to your 309 | actual package rather than the top level directory as this means you don't 310 | need to worry about skipping unrelated files like ``setup.py``. 311 | 312 | #. Configure ``apidoc_excluded_paths`` in ``conf.py`` 313 | 314 | The ``apidoc_excluded_paths`` setting in ``conf.py`` differs from the 315 | ``[pbr] autodoc_exclude_modules`` setting in ``setup.cfg`` in that the 316 | former is a list of fnmatch-style **file paths**, while the latter is a list 317 | of fnmatch-style **module paths**. As a result, you can reuse most of the 318 | values from the ``[pbr] autodoc_exclude_modules`` setting but you must 319 | switch from ``x.y`` format to ``x/y``. You may also need to update these 320 | paths if you configured ``apidoc_module_dir`` to point to something other 321 | than the top-level directory. 322 | 323 | #. Configure ``apidoc_output_dir`` in ``conf.py`` 324 | 325 | The ``apidoc_output_dir`` setting in ``conf.py`` works exactly like the 326 | ``[pbr] api_doc_dir`` setting in ``setup.cfg``; namely, it's a path relative 327 | to the documentation source directory to which all API documentation should 328 | be written. You can just copy the value from the ``[pbr] api_doc_dir`` 329 | setting. 330 | 331 | #. Configure ``apidoc_separate_modules=True`` in ``conf.py`` 332 | 333 | By default, *sphinx-apidoc* generates a document per package while *autodoc* 334 | generates a document per (sub)module. By setting this attribute to ``True``, 335 | we ensure the latter behavior is used. 336 | 337 | #. Replace references to ``autoindex.rst`` with ``modules.rst`` 338 | 339 | The *autodoc* feature generates a list of modules in a file called 340 | ``autoindex.rst`` located in the output directory. By comparison, 341 | *sphinx-apidoc* and this extension call this file ``modules.rst``. You must 342 | update all references to ``autoindex.rst`` with ``modules.rst`` instead. You 343 | may also wish to configure the ``depth`` option of any ``toctree``\s that 344 | include this document as ``modules.rst`` is nested. 345 | 346 | #. Remove settings from ``setup.cfg`` 347 | 348 | Remove the following settings from the ``[pbr]`` section of the 349 | ``setup.cfg`` file: 350 | 351 | - ``autodoc_index_modules`` 352 | - ``autodoc_exclude_modules`` 353 | - ``api_doc_dir`` 354 | 355 | You may also wish to remove the entirety of the ``[build_sphinx]`` section, 356 | should you wish to build docs using ``sphinx-build`` instead. 357 | 358 | Once done, your output should look similar to previously. The main change will 359 | be in the aforementioned ``modules.rst``, which uses a nested layout compared 360 | to the flat layout of the ``autoindex.rst`` file. 361 | 362 | Links 363 | ----- 364 | 365 | - Source: https://github.com/sphinx-contrib/apidoc 366 | - Bugs: https://github.com/sphinx-contrib/apidoc/issues 367 | 368 | .. Links 369 | 370 | .. _sphinx.ext.apidoc: https://www.sphinx-doc.org/en/master/usage/extensions/apidoc.html 371 | .. _sphinx-apidoc: http://www.sphinx-doc.org/en/stable/man/sphinx-apidoc.html 372 | .. _sphinx_autodoc: http://www.sphinx-doc.org/en/stable/ext/autodoc.html 373 | .. _pbr: https://docs.openstack.org/pbr/ 374 | .. _build_sphinx: https://docs.openstack.org/pbr/latest/user/using.html#build-sphinx 375 | .. _python-openstackclient: https://github.com/openstack/python-openstackclient/tree/3.15.0 376 | .. _oslo.privsep: https://github.com/openstack/oslo.privsep/tree/1.28.0 377 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.ruff] 2 | line-length = 88 3 | target-version = 'py39' 4 | 5 | [tool.ruff.format] 6 | quote-style = "preserve" 7 | docstring-code-format = true 8 | 9 | [tool.ruff.lint] 10 | select = ["E4", "E7", "E9", "F", "S", "UP"] 11 | ignore = [] 12 | 13 | [tool.ruff.lint.per-file-ignores] 14 | "tests/*" = ["S101"] 15 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | pbr 2 | Sphinx>=5.0.0 3 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | name = sphinxcontrib-apidoc 3 | summary = A Sphinx extension for running 'sphinx-apidoc' on each build 4 | description_file = README.rst 5 | description_content_type = text/x-rst; charset=UTF-8 6 | author = Stephen Finucane 7 | author_email = stephen@that.guru 8 | url = https://github.com/sphinx-contrib/apidoc 9 | project_urls = 10 | Bug Tracker = https://github.com/sphinx-contrib/apidoc/issues 11 | Source Code = https://github.com/sphinx-contrib/apidoc 12 | license = BSD License 13 | classifiers = 14 | Development Status :: 4 - Beta 15 | Environment :: Console 16 | Framework :: Sphinx :: Extension 17 | Intended Audience :: Developers 18 | Intended Audience :: Information Technology 19 | License :: OSI Approved :: BSD License 20 | Operating System :: OS Independent 21 | Programming Language :: Python 22 | Programming Language :: Python :: 3 23 | Programming Language :: Python :: 3 :: Only 24 | Topic :: Documentation 25 | Topic :: Documentation :: Sphinx 26 | Topic :: Utilities 27 | python_requires = >=3.9 28 | keywords = sphinx 29 | 30 | [files] 31 | packages = 32 | sphinxcontrib 33 | namespace_packages = 34 | sphinxcontrib 35 | 36 | [flake8] 37 | max-line-length = 88 38 | ignore = E203,E501,E741,W503 39 | 40 | [mypy] 41 | show_column_numbers = true 42 | show_error_context = true 43 | ignore_missing_imports = true 44 | follow_imports = skip 45 | incremental = true 46 | check_untyped_defs = true 47 | warn_unused_ignores = true 48 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import setuptools 4 | 5 | 6 | setuptools.setup( 7 | setup_requires=['pbr>=4.0'], 8 | pbr=True, 9 | ) 10 | -------------------------------------------------------------------------------- /sphinxcontrib/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | sphinxcontrib 3 | ~~~~~~~~~~~~~ 4 | 5 | This package is a namespace package that contains all extensions 6 | distributed in the ``sphinx-contrib`` distribution. 7 | 8 | :copyright: Copyright 2007-2009 by the Sphinx team, see AUTHORS. 9 | :license: BSD, see LICENSE for details. 10 | """ 11 | 12 | __import__('pkg_resources').declare_namespace(__name__) 13 | -------------------------------------------------------------------------------- /sphinxcontrib/apidoc/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | sphinxcontrib.apidoc 3 | ~~~~~~~~~~~~~~~~~~~~ 4 | 5 | A Sphinx extension for running 'sphinx-apidoc' on each build. 6 | 7 | :copyright: Copyright 2018-present by Stephen Finucane 8 | :license: BSD, see LICENSE for details. 9 | """ 10 | 11 | import pbr.version 12 | from typing import Any 13 | 14 | from sphinx.application import Sphinx 15 | from sphinxcontrib.apidoc import ext 16 | 17 | __version__ = pbr.version.VersionInfo('sphinxcontrib-apidoc').version_string() 18 | 19 | 20 | def setup(app: Sphinx) -> dict[str, Any]: 21 | app.setup_extension('sphinx.ext.autodoc') # We need autodoc to function 22 | 23 | app.connect('builder-inited', ext.builder_inited) 24 | app.add_config_value('apidoc_module_dir', None, 'env', [str]) 25 | app.add_config_value('apidoc_output_dir', 'api', 'env', [str]) 26 | app.add_config_value('apidoc_template_dir', 'templates', 'env', [str]) 27 | app.add_config_value('apidoc_excluded_paths', [], 'env', list[str]) 28 | app.add_config_value('apidoc_separate_modules', False, 'env', [bool]) 29 | app.add_config_value('apidoc_toc_file', None, 'env', [str, bool]) 30 | app.add_config_value('apidoc_module_first', False, 'env', [bool]) 31 | app.add_config_value('apidoc_extra_args', [], 'env', [list]) 32 | 33 | return {'version': __version__, 'parallel_read_safe': True} 34 | -------------------------------------------------------------------------------- /sphinxcontrib/apidoc/ext.py: -------------------------------------------------------------------------------- 1 | """ 2 | sphinxcontrib.apidoc.ext 3 | ~~~~~~~~~~~~~~~~~~~~~~~~ 4 | 5 | A Sphinx extension for running 'sphinx-apidoc' on each build. 6 | 7 | :copyright: Copyright 2018-present by Stephen Finucane 8 | :license: BSD, see LICENSE for details. 9 | """ 10 | 11 | from os import path 12 | 13 | from sphinx.application import Sphinx 14 | from sphinx.ext import apidoc 15 | from sphinx.util import logging 16 | 17 | logger = logging.getLogger(__name__) 18 | 19 | 20 | def builder_inited(app: Sphinx) -> None: 21 | module_dir = app.config.apidoc_module_dir 22 | output_dir = path.join(app.srcdir, app.config.apidoc_output_dir) 23 | template_dir = path.join(app.srcdir, app.config.apidoc_template_dir) 24 | excludes = app.config.apidoc_excluded_paths 25 | separate_modules = app.config.apidoc_separate_modules 26 | toc_file = app.config.apidoc_toc_file 27 | module_first = app.config.apidoc_module_first 28 | extra_args = app.config.apidoc_extra_args 29 | 30 | if not module_dir: 31 | logger.warning("No 'apidoc_module_dir' specified; skipping API doc generation") 32 | return 33 | 34 | # if the path is relative, make it relative to the 'conf.py' directory 35 | if not path.isabs(module_dir): 36 | module_dir = path.abspath(path.join(app.srcdir, module_dir)) 37 | 38 | if not path.exists(module_dir): 39 | logger.warning( 40 | "The path defined in 'apidoc_module_dir' does not " 41 | "exist; skipping API doc generation; %s", 42 | module_dir, 43 | ) 44 | return 45 | 46 | # refactor this module so that we can call 'recurse_tree' like a sane 47 | # person - at present there is way too much passing around of the 48 | # 'optparse.Value' instance returned by 'optparse.parse_args' 49 | def cmd_opts(): 50 | yield '--force' 51 | 52 | if separate_modules: 53 | yield '--separate' 54 | 55 | if isinstance(toc_file, bool) and toc_file is False: 56 | yield '--no-toc' 57 | elif toc_file: 58 | yield '--tocfile' 59 | yield toc_file 60 | 61 | if module_first: 62 | yield '--module-first' 63 | 64 | yield '--output-dir' 65 | yield output_dir 66 | 67 | yield '--templatedir' 68 | yield template_dir 69 | 70 | yield from extra_args 71 | 72 | yield module_dir 73 | 74 | for exc in excludes: 75 | yield path.abspath(path.join(module_dir, exc)) 76 | 77 | apidoc.main(list(cmd_opts())) 78 | -------------------------------------------------------------------------------- /test-requirements.txt: -------------------------------------------------------------------------------- 1 | pytest 2 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | """ 2 | pytest config for sphinxcontrib/apidoc/tests 3 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 4 | 5 | :copyright: Copyright 2018-present by Stephen Finucane 6 | :license: BSD, see LICENSE for details. 7 | """ 8 | 9 | import os 10 | import tempfile 11 | 12 | import pytest 13 | import sphinx 14 | 15 | pytest_plugins = 'sphinx.testing.fixtures' 16 | 17 | collect_ignore = ['roots'] 18 | 19 | 20 | @pytest.fixture(scope='session') 21 | def sphinx_test_tempdir(): 22 | if sphinx.version_info >= (7, 2, 0): 23 | from pathlib import Path 24 | 25 | return Path( 26 | os.environ.get('SPHINX_TEST_TEMPDIR', tempfile.mkdtemp(prefix='apidoc-')) 27 | ).resolve() 28 | else: 29 | from sphinx.testing.path import path 30 | 31 | return path( 32 | os.environ.get('SPHINX_TEST_TEMPDIR', tempfile.mkdtemp(prefix='apidoc-')) 33 | ).abspath() 34 | 35 | 36 | @pytest.fixture(scope='session') 37 | def rootdir(): 38 | if sphinx.version_info >= (7, 2, 0): 39 | from pathlib import Path 40 | 41 | return Path(os.path.dirname(__file__) or '.').resolve() / 'roots' 42 | else: 43 | from sphinx.testing.path import path 44 | 45 | return path(os.path.dirname(__file__) or '.').abspath() / 'roots' 46 | -------------------------------------------------------------------------------- /tests/roots/test-advanced-negative/apidoc_dummy_module.py: -------------------------------------------------------------------------------- 1 | from os import * # noqa 2 | 3 | from apidoc_dummy_package import apidoc_dummy_submodule_a as a # noqa 4 | from apidoc_dummy_package import apidoc_dummy_submodule_b as b # noqa 5 | 6 | 7 | class Foo: 8 | def __init__(self): 9 | pass 10 | 11 | def bar(self): 12 | pass 13 | -------------------------------------------------------------------------------- /tests/roots/test-advanced-negative/apidoc_dummy_package/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sphinx-contrib/apidoc/f5f1ec8366143d013355fa40341442bc1e217916/tests/roots/test-advanced-negative/apidoc_dummy_package/__init__.py -------------------------------------------------------------------------------- /tests/roots/test-advanced-negative/apidoc_dummy_package/apidoc_dummy_submodule_a.py: -------------------------------------------------------------------------------- 1 | class Bar: 2 | def foo(self): 3 | print('Hello, world') 4 | -------------------------------------------------------------------------------- /tests/roots/test-advanced-negative/apidoc_dummy_package/apidoc_dummy_submodule_b.py: -------------------------------------------------------------------------------- 1 | class Baz: 2 | def inga(self): 3 | return 1 + 3 4 | -------------------------------------------------------------------------------- /tests/roots/test-advanced-negative/conf.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | sys.path.insert(0, os.path.abspath('./')) 5 | 6 | extensions = ['sphinxcontrib.apidoc'] 7 | master_doc = 'index' 8 | 9 | apidoc_module_dir = '.' 10 | apidoc_excluded_paths = ['conf.py'] 11 | apidoc_separate_modules = False 12 | apidoc_toc_file = False 13 | apidoc_module_first = False 14 | -------------------------------------------------------------------------------- /tests/roots/test-advanced-negative/index.rst: -------------------------------------------------------------------------------- 1 | apidoc 2 | ====== 3 | 4 | .. toctree:: 5 | 6 | api/apidoc_dummy_module 7 | api/apidoc_dummy_package 8 | -------------------------------------------------------------------------------- /tests/roots/test-advanced/apidoc_dummy_module.py: -------------------------------------------------------------------------------- 1 | from os import * # noqa 2 | 3 | from apidoc_dummy_package import apidoc_dummy_submodule_a as a # noqa 4 | from apidoc_dummy_package import apidoc_dummy_submodule_b as b # noqa 5 | 6 | 7 | class Foo: 8 | def __init__(self): 9 | pass 10 | 11 | def bar(self): 12 | pass 13 | -------------------------------------------------------------------------------- /tests/roots/test-advanced/apidoc_dummy_package/__init__.py: -------------------------------------------------------------------------------- 1 | def bogus(): 2 | return 'bogus' 3 | -------------------------------------------------------------------------------- /tests/roots/test-advanced/apidoc_dummy_package/_apidoc_private_dummy_submodule.py: -------------------------------------------------------------------------------- 1 | def very_private(): 2 | return 'private' 3 | -------------------------------------------------------------------------------- /tests/roots/test-advanced/apidoc_dummy_package/apidoc_dummy_submodule_a.py: -------------------------------------------------------------------------------- 1 | class Bar: 2 | def foo(self): 3 | print('Hello, world') 4 | -------------------------------------------------------------------------------- /tests/roots/test-advanced/apidoc_dummy_package/apidoc_dummy_submodule_b.py: -------------------------------------------------------------------------------- 1 | class Baz: 2 | def inga(self): 3 | return 1 + 3 4 | -------------------------------------------------------------------------------- /tests/roots/test-advanced/conf.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | sys.path.insert(0, os.path.abspath('./')) 5 | 6 | extensions = ['sphinxcontrib.apidoc'] 7 | master_doc = 'index' 8 | 9 | apidoc_module_dir = '.' 10 | apidoc_excluded_paths = ['conf.py'] 11 | apidoc_separate_modules = True 12 | apidoc_toc_file = 'custom' 13 | apidoc_module_first = True 14 | apidoc_extra_args = ['--private'] 15 | -------------------------------------------------------------------------------- /tests/roots/test-advanced/index.rst: -------------------------------------------------------------------------------- 1 | apidoc 2 | ====== 3 | 4 | .. toctree:: 5 | 6 | api/custom 7 | -------------------------------------------------------------------------------- /tests/roots/test-basics/apidoc_dummy_module.py: -------------------------------------------------------------------------------- 1 | from os import * # noqa 2 | 3 | 4 | class Foo: 5 | def __init__(self): 6 | pass 7 | 8 | def bar(self): 9 | pass 10 | -------------------------------------------------------------------------------- /tests/roots/test-basics/conf.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | sys.path.insert(0, os.path.abspath('./')) 5 | 6 | extensions = ['sphinxcontrib.apidoc'] 7 | master_doc = 'index' 8 | 9 | apidoc_module_dir = '.' 10 | apidoc_excluded_paths = ['conf.py'] 11 | -------------------------------------------------------------------------------- /tests/roots/test-basics/index.rst: -------------------------------------------------------------------------------- 1 | apidoc 2 | ====== 3 | 4 | .. toctree:: 5 | 6 | api/modules 7 | -------------------------------------------------------------------------------- /tests/roots/test-invalid-directory/apidoc_dummy_module.py: -------------------------------------------------------------------------------- 1 | from os import * # noqa 2 | 3 | 4 | class Foo: 5 | def __init__(self): 6 | pass 7 | 8 | def bar(self): 9 | pass 10 | -------------------------------------------------------------------------------- /tests/roots/test-invalid-directory/conf.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | sys.path.insert(0, os.path.abspath('./')) 5 | 6 | extensions = ['sphinxcontrib.apidoc'] 7 | master_doc = 'index' 8 | 9 | apidoc_module_dir = 'foobar' 10 | apidoc_excluded_paths = ['conf.py'] 11 | -------------------------------------------------------------------------------- /tests/roots/test-invalid-directory/index.rst: -------------------------------------------------------------------------------- 1 | apidoc 2 | ====== 3 | 4 | .. toctree:: 5 | 6 | api/modules 7 | -------------------------------------------------------------------------------- /tests/roots/test-missing-configuration/apidoc_dummy_module.py: -------------------------------------------------------------------------------- 1 | from os import * # noqa 2 | 3 | 4 | class Foo: 5 | def __init__(self): 6 | pass 7 | 8 | def bar(self): 9 | pass 10 | -------------------------------------------------------------------------------- /tests/roots/test-missing-configuration/conf.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | sys.path.insert(0, os.path.abspath('./')) 5 | 6 | extensions = ['sphinxcontrib.apidoc'] 7 | master_doc = 'index' 8 | -------------------------------------------------------------------------------- /tests/roots/test-missing-configuration/index.rst: -------------------------------------------------------------------------------- 1 | apidoc 2 | ====== 3 | 4 | .. toctree:: 5 | 6 | api/modules 7 | -------------------------------------------------------------------------------- /tests/test_ext.py: -------------------------------------------------------------------------------- 1 | """ 2 | test_apidoc 3 | ~~~~~~~~~~~ 4 | 5 | Test the sphinxcontrib.apidoc module. 6 | 7 | :copyright: Copyright 2018-present by Stephen Finucane 8 | :license: BSD, see LICENSE for details. 9 | """ 10 | 11 | import pytest 12 | import sphinx 13 | from sphinx.util import logging 14 | 15 | 16 | def is_dir(path): 17 | if sphinx.version_info >= (7, 2, 0): 18 | return path.is_dir() 19 | else: 20 | return path.isdir() 21 | 22 | 23 | @pytest.mark.sphinx('html', testroot='basics') 24 | def test_basics(app, status, warning): 25 | logging.setup(app, status, warning) 26 | app.builder.build_all() 27 | 28 | assert is_dir(app.srcdir / 'api') 29 | assert (app.srcdir / 'api' / 'modules.rst').exists() 30 | assert (app.srcdir / 'api' / 'apidoc_dummy_module.rst').exists() 31 | assert not (app.srcdir / 'api' / 'conf.rst').exists() 32 | 33 | assert is_dir(app.outdir / 'api') 34 | assert (app.outdir / 'api' / 'modules.html').exists() 35 | assert (app.outdir / 'api' / 'apidoc_dummy_module.html').exists() 36 | assert not (app.outdir / 'api' / 'conf.html').exists() 37 | 38 | assert not warning.getvalue() 39 | 40 | 41 | @pytest.mark.sphinx('html', testroot='advanced') 42 | def test_advanced(app, status, warning): 43 | if sphinx.version_info < (1, 8, 0): 44 | pytest.xfail('This should fail on older Sphinx versions') 45 | 46 | logging.setup(app, status, warning) 47 | app.builder.build_all() 48 | 49 | assert is_dir(app.srcdir / 'api') 50 | assert (app.srcdir / 'api' / 'custom.rst').exists() 51 | for module in [ 52 | 'apidoc_dummy_module.rst', 53 | 'apidoc_dummy_package.apidoc_dummy_submodule_a.rst', 54 | 'apidoc_dummy_package.apidoc_dummy_submodule_b.rst', 55 | 'apidoc_dummy_package._apidoc_private_dummy_submodule.rst', 56 | ]: 57 | assert (app.srcdir / 'api' / module).exists() 58 | assert (app.srcdir / 'api' / 'apidoc_dummy_package.rst').exists() 59 | assert not (app.srcdir / 'api' / 'conf.rst').exists() 60 | 61 | with open(app.srcdir / 'api' / 'apidoc_dummy_package.rst') as fh: 62 | package_doc = [x.strip() for x in fh.readlines()] 63 | 64 | # The 'Module contents' header isn't present if '--module-first' used 65 | assert 'Module contents' not in package_doc 66 | 67 | assert is_dir(app.outdir / 'api') 68 | assert (app.outdir / 'api' / 'custom.html').exists() 69 | for module in [ 70 | 'apidoc_dummy_module.html', 71 | 'apidoc_dummy_package.apidoc_dummy_submodule_a.html', 72 | 'apidoc_dummy_package.apidoc_dummy_submodule_b.html', 73 | 'apidoc_dummy_package._apidoc_private_dummy_submodule.html', 74 | ]: 75 | assert (app.outdir / 'api' / module).exists() 76 | assert (app.outdir / 'api' / 'apidoc_dummy_package.html').exists() 77 | assert not (app.outdir / 'api' / 'conf.html').exists() 78 | 79 | assert not warning.getvalue() 80 | 81 | 82 | @pytest.mark.sphinx('html', testroot='advanced-negative') 83 | def test_advanced_negative(app, status, warning): 84 | """The "test_advanced" test but with boolean options toggled.""" 85 | logging.setup(app, status, warning) 86 | app.builder.build_all() 87 | 88 | assert is_dir(app.srcdir / 'api') 89 | for module in [ 90 | 'apidoc_dummy_module.rst', 91 | ]: 92 | assert (app.srcdir / 'api' / module).exists() 93 | assert (app.srcdir / 'api' / 'apidoc_dummy_package.rst').exists() 94 | assert not (app.srcdir / 'api' / 'custom.rst').exists() 95 | assert not (app.srcdir / 'api' / 'conf.rst').exists() 96 | 97 | with open(app.srcdir / 'api' / 'apidoc_dummy_package.rst') as fh: 98 | package_doc = [x.strip() for x in fh.readlines()] 99 | 100 | # The 'Module contents' header is present if '--module-first' isn't used 101 | assert 'Module contents' in package_doc 102 | 103 | assert is_dir(app.outdir / 'api') 104 | for module in [ 105 | 'apidoc_dummy_module.html', 106 | ]: 107 | assert (app.outdir / 'api' / module).exists() 108 | assert (app.outdir / 'api' / 'apidoc_dummy_package.html').exists() 109 | assert not (app.outdir / 'api' / 'custom.html').exists() 110 | assert not (app.outdir / 'api' / 'conf.html').exists() 111 | 112 | assert not warning.getvalue() 113 | 114 | 115 | @pytest.mark.sphinx('html', testroot='missing-configuration') 116 | def test_missing_configuration(app, status, warning): 117 | logging.setup(app, status, warning) 118 | app.builder.build_all() 119 | assert not (app.outdir / 'api').exists() 120 | assert "No 'apidoc_module_dir' specified" in warning.getvalue() 121 | 122 | 123 | @pytest.mark.sphinx('html', testroot='invalid-directory') 124 | def test_invalid_directory(app, status, warning): 125 | logging.setup(app, status, warning) 126 | app.builder.build_all() 127 | assert not (app.outdir / 'api').exists() 128 | assert "The path defined in 'apidoc_module_dir'" in warning.getvalue() 129 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | minversion = 4.4 3 | envlist = py{38,39,310,311},style 4 | 5 | [testenv] 6 | setenv = 7 | PYTHONDEVMODE = 1 8 | PYTHONWARNINGS = all 9 | PYTEST_ADDOPTS = {env:PYTEST_ADDOPTS:} --color yes 10 | deps = 11 | -r{toxinidir}/test-requirements.txt 12 | commands= 13 | pytest 14 | 15 | [testenv:style] 16 | description = 17 | Run style checks. 18 | deps = 19 | pre-commit 20 | commands = 21 | pre-commit run --all-files --show-diff-on-failure 22 | --------------------------------------------------------------------------------