├── .gitignore ├── requirements.txt ├── mkdocs_autoapi ├── __init__.py ├── literate_nav │ ├── __init__.py │ ├── exceptions.py │ ├── globber.py │ ├── resolve.py │ └── parser.py ├── section_index │ ├── __init__.py │ ├── section_page.py │ └── rewrite.py ├── generate_files │ ├── __init__.py │ ├── nav.py │ └── editor.py ├── logging.py ├── autoapi.py └── plugin.py ├── .readthedocs.yaml ├── .pre-commit-config.yaml ├── mkdocs.yml ├── LICENSE ├── .github └── workflows │ ├── publish-to-pypi.yml │ └── create-release.yml ├── docs ├── index.md └── usage.md ├── CHANGELOG.md ├── pyproject.toml ├── README.md └── CONTRIBUTING.md /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | site/ 3 | dist/ 4 | *.egg-info/ -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | mkdocs>=1.4.0 2 | mkdocstrings[python]>=0.19.0 3 | pre-commit>=3.5.0 4 | ruff>=0.6.2 5 | -------------------------------------------------------------------------------- /mkdocs_autoapi/__init__.py: -------------------------------------------------------------------------------- 1 | """Init file for the mkdocs_autoapi module. 2 | 3 | As I work through the build, I'll update the documentation for this module. 4 | """ 5 | -------------------------------------------------------------------------------- /mkdocs_autoapi/literate_nav/__init__.py: -------------------------------------------------------------------------------- 1 | """Init file for the literate_nav module. 2 | 3 | As I work through the build, I'll update the documentation for this module. 4 | """ 5 | -------------------------------------------------------------------------------- /mkdocs_autoapi/section_index/__init__.py: -------------------------------------------------------------------------------- 1 | """Init file for the section_index module. 2 | 3 | As I work through the build, I'll update the documentation for this module. 4 | """ 5 | -------------------------------------------------------------------------------- /mkdocs_autoapi/literate_nav/exceptions.py: -------------------------------------------------------------------------------- 1 | """Literate nav exceptions.""" 2 | 3 | 4 | class LiterateNavError(Exception): 5 | """An error occurred while building a literate nav.""" 6 | 7 | pass 8 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | build: 4 | os: ubuntu-lts-latest 5 | tools: 6 | python: "3.8" 7 | 8 | mkdocs: 9 | configuration: mkdocs.yml 10 | 11 | python: 12 | install: 13 | - requirements: requirements.txt -------------------------------------------------------------------------------- /mkdocs_autoapi/generate_files/__init__.py: -------------------------------------------------------------------------------- 1 | """Init file for the generate_files module. 2 | 3 | As I work through the build, I'll update the documentation for this module. 4 | """ 5 | 6 | from .editor import FilesEditor 7 | from .nav import Nav as Nav 8 | 9 | 10 | def __getattr__(name: str): 11 | return getattr(FilesEditor.current(), name) 12 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | # pre-commit configuration 2 | 3 | repos: 4 | - repo: https://github.com/pre-commit/pre-commit-hooks 5 | rev: v4.6.0 6 | hooks: 7 | - id: check-yaml 8 | - id: end-of-file-fixer 9 | - id: trailing-whitespace 10 | - repo: https://github.com/astral-sh/ruff-pre-commit 11 | rev: v0.6.2 12 | hooks: 13 | - id: ruff 14 | args: [--fix] 15 | - id: ruff-format 16 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: "mkdocs-autoapi" 2 | site_description: "MkDocs plugin providing automatic API reference generation" 3 | site_author: "Jacob Ayers" 4 | repo_url: "https://github.com/jcayers20/mkdocs-autoapi" 5 | edit_uri: "edit/feature-readthedocs/docs/" 6 | 7 | markdown_extensions: 8 | - admonition 9 | - pymdownx.details 10 | - pymdownx.superfences 11 | - toc: 12 | permalink: "#" 13 | 14 | nav: 15 | - 'Home': 'index.md' 16 | - 'Usage': 'usage.md' 17 | 18 | theme: 19 | name: readthedocs 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Jacob Ayers 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /mkdocs_autoapi/section_index/section_page.py: -------------------------------------------------------------------------------- 1 | """Section page definition.""" 2 | 3 | # built-in imports 4 | 5 | # third-party imports 6 | from mkdocs.structure.nav import Section 7 | from mkdocs.structure.pages import Page 8 | 9 | 10 | class SectionPage(Section, Page): 11 | """A page that is also a section.""" 12 | 13 | def __init__(self, title: str, file, config, children): 14 | """Initialize a SectionPage instance.""" 15 | Page.__init__(self, title=title, file=file, config=config) 16 | Section.__init__(self, title=title, children=children) 17 | self.is_section = self.is_page = True 18 | 19 | active = Page.active # type: ignore[assignment] 20 | 21 | def __repr__(self): 22 | """Return a string representation of the SectionPage instance.""" 23 | result = Page.__repr__(self) 24 | if not result.startswith("Section"): 25 | result = "Section" + result 26 | return result 27 | 28 | def __eq__(self, other): 29 | """Check whether SectionPage instance is equal to another object.""" 30 | return object.__eq__(self, other) 31 | 32 | def __ne__(self, other): 33 | """Check whether SectionPage instance is not equal to another object.""" 34 | return not (self == other) 35 | 36 | def __hash__(self): 37 | """Get the hash of the SectionPage instance.""" 38 | return object.__hash__(self) 39 | -------------------------------------------------------------------------------- /.github/workflows/publish-to-pypi.yml: -------------------------------------------------------------------------------- 1 | name: Upload Python Package 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | permissions: 8 | contents: read 9 | 10 | jobs: 11 | deploy: 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v4 16 | - name: Set up Python 17 | uses: actions/setup-python@v3 18 | with: 19 | python-version: '3.x' 20 | - name: Install dependencies 21 | run: | 22 | python -m pip install --upgrade pip 23 | pip install build 24 | - name: Build package 25 | run: python -m build 26 | - name: Publish package 27 | uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29 28 | with: 29 | user: __token__ 30 | password: ${{ secrets.PYPI_API_TOKEN }} 31 | - name: Close milestone 32 | env: 33 | GH_TOKEN: ${{ secrets.GH_TOKEN }} 34 | run: | 35 | version=${{ github.event.release.tag_name }} 36 | milestone_number=$(curl -s -H "Authorization: token $GH_TOKEN" \ 37 | https://api.github.com/repos/${{ github.repository }}/milestones \ 38 | | jq -r ".[] | select(.title==\"$version\") | .number") 39 | if [ -n "$milestone_number" ]; then 40 | curl -s -X PATCH -H "Authorization: token $GH_TOKEN" \ 41 | -d '{"state":"closed"}' \ 42 | https://api.github.com/repos/${{ github.repository }}/milestones/$milestone_number 43 | else 44 | echo "Milestone $version not found" 45 | fi 46 | -------------------------------------------------------------------------------- /mkdocs_autoapi/logging.py: -------------------------------------------------------------------------------- 1 | """Logging utilities.""" 2 | 3 | import logging 4 | from typing import Any, MutableMapping, Tuple 5 | 6 | 7 | class AutoApiLogger(logging.LoggerAdapter): 8 | """A logger adapter to prefix messages with the originating package name.""" 9 | 10 | def __init__(self, prefix: str, logger: logging.Logger): 11 | """Initialize the object. 12 | 13 | Arguments: 14 | prefix: The string to insert in front of every message. 15 | logger: The logger instance. 16 | """ 17 | super().__init__(logger, {}) 18 | self.prefix = prefix 19 | 20 | def process( 21 | self, msg: str, kwargs: MutableMapping[str, Any] 22 | ) -> Tuple[str, Any]: 23 | """Process the message. 24 | 25 | Args: 26 | msg: 27 | The message. 28 | kwargs: 29 | Remaining arguments. 30 | 31 | Returns: 32 | The processed message. 33 | """ 34 | return f"{self.prefix}: {msg}", kwargs 35 | 36 | 37 | def get_logger(name: str) -> AutoApiLogger: 38 | """Return a logger for plugins. 39 | 40 | Arguments: 41 | name: The name to use with `logging.getLogger`. 42 | 43 | Returns: 44 | A logger configured to work well in MkDocs, 45 | prefixing each message with the plugin package name. 46 | """ 47 | logger = logging.getLogger(f"mkdocs.plugins.{name}") 48 | return AutoApiLogger( 49 | prefix=name.split(".", 1)[0], 50 | logger=logger, 51 | ) 52 | -------------------------------------------------------------------------------- /.github/workflows/create-release.yml: -------------------------------------------------------------------------------- 1 | name: Create Release 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | create_release: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - name: Checkout code 14 | uses: actions/checkout@v4 15 | 16 | - name: Set up Python 17 | uses: actions/setup-python@v3 18 | with: 19 | python-version: '3.x' 20 | 21 | - name: Extract version from pyproject.toml 22 | id: extract_version 23 | run: | 24 | version=$(sed -n 's/^version = "\([^"]*\)"/\1/p' pyproject.toml) 25 | echo "VERSION=$version" >> $GITHUB_ENV 26 | 27 | - name: Get pull request description 28 | id: get_pr_description 29 | uses: actions/github-script@v6 30 | with: 31 | github-token: ${{ secrets.GH_TOKEN }} 32 | script: | 33 | const pr = await github.rest.pulls.list({ 34 | owner: context.repo.owner, 35 | repo: context.repo.repo, 36 | state: 'closed', 37 | base: 'main', 38 | sort: 'updated', 39 | direction: 'desc', 40 | per_page: 1 41 | }); 42 | const description = pr.data[0].body || 'No description provided'; 43 | return description; 44 | result-encoding: string 45 | 46 | - name: Create tag 47 | id: create_tag 48 | uses: rickstaa/action-create-tag@v1 49 | with: 50 | tag: "${{ env.VERSION }}" 51 | # message: "${{ steps.get_pr_description.outputs.result }}" 52 | github_token: ${{ secrets.GH_TOKEN }} 53 | 54 | - name: Create GitHub release 55 | uses: actions/create-release@v1 56 | env: 57 | GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} 58 | with: 59 | tag_name: "${{ env.VERSION }}" 60 | release_name: "${{ env.VERSION }}" 61 | body: "${{ steps.get_pr_description.outputs.result }}" 62 | draft: false 63 | prerelease: false 64 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # mkdocs-autoapi 2 | 3 | `mkdocs-autoapi` is a plugin for [MkDocs](https://www.mkdocs.org) that generates 4 | API documentation from your project's source code. The plugin leverages the 5 | functionality provided by [mkdocstrings](https://mkdocstrings.github.io/) and 6 | locates all Python modules in your project to create a set of reference pages. 7 | 8 | ## Installation 9 | 10 | ### Requirements 11 | 12 | `mkdocs-autoapi` requires the following: 13 | 14 | * Python version 3.6 or higher 15 | * MkDocs version 1.4.0 or higher 16 | * mkdocstrings version 0.19.0 or higher 17 | 18 | In addition, you must install an `mkdocstrings` 19 | [handler](https://mkdocstrings.github.io/usage/handlers/) for your project's 20 | programming language. 21 | 22 | !!! note 23 | Currently, Python and VBA handlers are supported. Support for additional 24 | programming languages (e.g., C, shell) is planned for future releases. 25 | See [Installation via `pip`](#installation-via-pip) for more details on how 26 | to install handlers along with `mkdocs-autoapi`. 27 | 28 | ### Installation via `pip` 29 | 30 | To install `mkdocs-autoapi` with `pip`: 31 | 32 | ```bash 33 | pip install mkdocs-autoapi 34 | ``` 35 | 36 | Extras are provided to support installation of `mkdocstrings` handlers: 37 | 38 | ```bash 39 | pip install mkdocs-autoapi[python] # new Python handler 40 | ``` 41 | 42 | ```bash 43 | pip install mkdocs-autoapi[python-legacy] # legacy Python handler 44 | ``` 45 | 46 | ```bash 47 | pip install mkdocs-autoapi[vba] # VBA handler 48 | ``` 49 | 50 | ## Basic Usage 51 | 52 | To get started using `mkdocs-autoapi`, add the following to `mkdocs.yml`: 53 | 54 | ```yaml title="mkdocs.yml" 55 | 56 | nav: 57 | - ... other navigation configuration ... 58 | - API Reference: autoapi/ 59 | - ... other navigation configuration ... 60 | 61 | plugins: 62 | - ... other plugin configuration ... 63 | - mkdocs-autoapi 64 | - mkdocstrings 65 | ``` 66 | 67 | For details on configuration and examples, see the [Usage](usage.md) section. 68 | -------------------------------------------------------------------------------- /mkdocs_autoapi/literate_nav/globber.py: -------------------------------------------------------------------------------- 1 | """Definiton of the globber class.""" 2 | 3 | # built-in imports 4 | import fnmatch 5 | import re 6 | from pathlib import PurePosixPath 7 | from typing import Iterator, Union 8 | 9 | # local imports 10 | from mkdocs.structure.files import Files 11 | 12 | 13 | class MkDocsGlobber: 14 | """Globber for MkDocs files.""" 15 | 16 | def __init__(self, files: Files): 17 | """Initialize an MkDocsGlobber object. 18 | 19 | Args: 20 | files: 21 | The MkDocs files object. 22 | """ 23 | self.files = {} 24 | self.dirs = {} 25 | self.index_dirs = {} 26 | 27 | for f in files: 28 | if not f.is_documentation_page(): 29 | continue 30 | 31 | path = PurePosixPath("/", f.src_uri) 32 | self.files[path] = True 33 | tail, head = path.parent, path.name 34 | 35 | if f.name == "index": 36 | self.index_dirs[tail] = path 37 | 38 | while True: 39 | self.dirs[tail] = True 40 | 41 | if not head: 42 | break 43 | 44 | tail, head = tail.parent, tail.name 45 | 46 | def isdir(self, path: str) -> bool: 47 | """Check if `path` is a directory.""" 48 | return PurePosixPath("/", path) in self.dirs 49 | 50 | def glob(self, pattern: str) -> Iterator[str]: 51 | """Glob `pattern`.""" 52 | pat_parts = PurePosixPath("/" + pattern).parts 53 | re_parts = [re.compile(fnmatch.translate(part)) for part in pat_parts] 54 | 55 | for collection in self.files, self.dirs: 56 | for path in collection: 57 | if len(path.parts) == len(re_parts): 58 | zipped = zip(path.parts, re_parts) 59 | next( 60 | zipped 61 | ) # Both path and pattern have a slash as their first part. 62 | if all(re_part.match(part) for part, re_part in zipped): 63 | yield str(path)[1:] 64 | 65 | def find_index(self, root: str) -> Union[str, None]: 66 | """Find the index file for `root`.""" 67 | root_path = PurePosixPath("/", root) 68 | if root_path in self.index_dirs: 69 | return str(self.index_dirs[root_path])[1:] 70 | return None 71 | -------------------------------------------------------------------------------- /mkdocs_autoapi/literate_nav/resolve.py: -------------------------------------------------------------------------------- 1 | """Logic to resolve directories in navigation.""" 2 | 3 | # built-in imports 4 | import os 5 | from typing import Optional, Tuple, Union 6 | 7 | # third-party imports 8 | import mkdocs.structure 9 | from mkdocs.structure.pages import Page 10 | 11 | # local imports 12 | from mkdocs_autoapi.generate_files.editor import Files 13 | from mkdocs_autoapi.literate_nav import parser 14 | from mkdocs_autoapi.literate_nav.globber import MkDocsGlobber 15 | 16 | 17 | def resolve_directories_in_nav( 18 | nav_data, 19 | files: Files, 20 | nav_file_name: str, 21 | implicit_index: bool, 22 | markdown_config: Optional[dict] = None, 23 | ): 24 | """Replace `directory/` references in MkDocs nav config. 25 | 26 | Directories, if found, are resolved by the rules of literate nav insertion: 27 | If it has a literate nav file, that is used. Otherwise, an implicit nav is 28 | generated. 29 | """ 30 | 31 | def get_nav_for_dir(path: str) -> Union[Tuple[str, str], None]: 32 | file = files.get_file_from_path(os.path.join(path, nav_file_name)) 33 | if not file: 34 | return None 35 | 36 | # Prevent the warning in case the user doesn't also end up including 37 | # this page in the final nav, maybe they want it only for the purpose of 38 | # feeding to this plugin. 39 | try: # MkDocs 1.5+ 40 | if file.inclusion.is_in_nav(): 41 | file.inclusion = ( 42 | mkdocs.structure.files.InclusionLevel.NOT_IN_NAV 43 | ) 44 | except AttributeError: 45 | # https://github.com/mkdocs/mkdocs/blob/ff0b726056/mkdocs/structure/nav.py#L113 46 | Page(None, file, {}) # type: ignore[arg-type] 47 | 48 | # https://github.com/mkdocs/mkdocs/blob/fa5aa4a26e/mkdocs/structure/pages.py#L120 49 | with open(file.abs_src_path, encoding="utf-8-sig") as f: 50 | return nav_file_name, f.read() 51 | 52 | globber = MkDocsGlobber(files) 53 | nav_parser = parser.NavParser( 54 | get_nav_for_dir, 55 | globber, 56 | implicit_index=implicit_index, 57 | markdown_config=markdown_config, 58 | ) 59 | 60 | result = None 61 | if not nav_data or get_nav_for_dir("."): 62 | result = nav_parser.markdown_to_nav() 63 | return result or nav_parser.resolve_yaml_nav(nav_data or []) 64 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on 6 | [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) and this project 7 | adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 8 | 9 | ## 0.4.1 - 2025-04-01 10 | 11 | [View Changes on GitHub](https://github.com/jcayers20/mkdocs-autoapi/compare/0.4.0...0.4.1) 12 | 13 | ### Bug Fixes 14 | 15 | - Fixed a bug where the plugin would crash if `nav` is not defined in 16 | `mkdocs.yml` but `autoapi_add_nav_entry` is not `False` (thanks @k4lizen for the 17 | catch!) 18 | 19 | ## 0.4.0 - 2025-02-05 20 | 21 | [View Changes on GitHub](https://github.com/jcayers20/mkdocs-autoapi/compare/0.3.2...0.4.0) 22 | 23 | ### Bug Fixes 24 | 25 | - Fixed `mkdocs` crash if both `repo_url` and `edit_uri` were provided in 26 | `mkdocs.yml` 27 | - Fixed navigation when using the `mkdocs` theme 28 | 29 | ### Features 30 | 31 | - Enabled VBA support 32 | 33 | ## 0.3.2 - 2024-11-01 34 | 35 | [View Changes on GitHub](https://github.com/jcayers20/mkdocs-autoapi/compare/0.3.1...0.3.2) 36 | 37 | ### Bug Fixes 38 | 39 | - Fixed incorrect optional dependency configuration 40 | 41 | ## 0.3.1 - 2024-10-03 42 | 43 | [View Changes on GitHub](https://github.com/jcayers20/mkdocs-autoapi/compare/0.3.0...0.3.1) 44 | 45 | ### Developer Support 46 | 47 | - Added GitHub Actions for automated package publishing and release/milestone 48 | management on push to the `main` branch 49 | 50 | ## 0.3.0 - 2024-09-30 51 | 52 | [View Changes on GitHub](https://github.com/jcayers20/mkdocs-autoapi/compare/0.2.1...0.3.0) 53 | 54 | ### Developer Support 55 | 56 | - Added pre-commit hooks for code formatting and linting 57 | - Added a development guide (`CONTRIBUTING.md`) to the documentation 58 | - Added some logging to the plugin to help with debugging 59 | 60 | ### Features 61 | 62 | - Renamed all existing configuration options to align with names used in 63 | `sphinx-autoapi`: 64 | - `project_root` to `autoapi_dir` 65 | - `exclude` to `autoapi_ignore` 66 | - `generate_local_output` to `autoapi_keep_files` 67 | - `output_dir` to `autoapi_root` 68 | - Added new configuration options based on `sphinx-autoapi`: 69 | - `autoapi_file_patterns`: Define which files to include in the auto-generated 70 | documentation 71 | - `autoapi_generate_api_docs`: Define whether to generate API documentation 72 | - `autoapi_add_nav_entry`: Define whether to add a navigation entry for the 73 | API documentation 74 | 75 | 76 | ## 0.2.1 - 2024-08-27 77 | 78 | [View Changes on GitHub](https://github.com/jcayers20/mkdocs-autoapi/compare/0.2.0...0.2.1) 79 | 80 | ### Bug Fixes 81 | 82 | - Fixed an ill-formed link in the requirements documentation 83 | - Fixed a typo in a function name (this function was not user-facing) 84 | 85 | 86 | ## 0.2.0 - 2024-08-26 87 | 88 | [View Changes on GitHub](https://github.com/jcayers20/mkdocs-autoapi/compare/0.1.6...0.2.0) 89 | 90 | ### Features 91 | 92 | - Added documentation 93 | - Added new configuration options: 94 | - `generate_local_output`: Define whether to create local copies of the 95 | auto-generated API documentation 96 | - `output_dir`: Define the directory in which the auto-generated API 97 | documentation will be stored 98 | - Various bug fixes and improvements 99 | 100 | 101 | ## 0.1.6 - 2024-06-06 102 | 103 | [View on GitHub](https://github.com/jcayers20/mkdocs-autoapi/tree/0.1.6) 104 | 105 | First working version of the plugin. 106 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools>=61.0.0"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [project] 6 | name = "mkdocs-autoapi" 7 | version = "0.4.1" 8 | description = "MkDocs plugin providing automatic API reference generation" 9 | dependencies = [ 10 | "mkdocs>=1.4.0", 11 | "mkdocstrings>=0.19.0", 12 | ] 13 | readme = "README.md" 14 | requires-python = ">=3.6" 15 | authors = [ 16 | { name = "Jacob Ayers", email = "jcayers20@gmail.com" } 17 | ] 18 | keywords = ["mkdocs", "documentation", "api", "autoapi"] 19 | classifiers = [ 20 | "Development Status :: 4 - Beta", 21 | "Intended Audience :: Developers", 22 | "License :: OSI Approved :: MIT License", 23 | "Programming Language :: Python :: 3", 24 | "Programming Language :: Python :: 3.6", 25 | "Programming Language :: Python :: 3.7", 26 | "Programming Language :: Python :: 3.8", 27 | "Programming Language :: Python :: 3.9", 28 | "Programming Language :: Python :: 3.10", 29 | "Programming Language :: Python :: 3.11", 30 | "Programming Language :: Python :: 3.12", 31 | ] 32 | 33 | [project.entry-points.'mkdocs.plugins'] 34 | mkdocs-autoapi = 'mkdocs_autoapi.plugin:AutoApiPlugin' 35 | 36 | [project.optional-dependencies] 37 | python-legacy = ["mkdocstrings[python-legacy]>=0.19.0"] 38 | python = ["mkdocstrings[python]>=0.19.0"] 39 | vba = ["mkdocstrings-vba>=0.0.10"] 40 | 41 | [project.urls] 42 | Changelog = "https://github.com/jcayers20/mkdocs-autoapi/blob/main/CHANGELOG.md" 43 | Documentation = "https://mkdocs-autoapi.readthedocs.io/en/0.2.1/" 44 | Repository = "https://github.com/jcayers20/mkdocs-autoapi" 45 | Issues = "https://github.com/jcayers20/mkdocs-autoapi/issues" 46 | License = "https://github.com/jcayers20/mkdocs-autoapi/blob/main/LICENSE" 47 | 48 | [tool.ruff] 49 | # Exclude a variety of commonly ignored directories. 50 | exclude = [ 51 | ".bzr", 52 | ".direnv", 53 | ".eggs", 54 | ".git", 55 | ".git-rewrite", 56 | ".hg", 57 | ".ipynb_checkpoints", 58 | ".mypy_cache", 59 | ".nox", 60 | ".pants.d", 61 | ".pyenv", 62 | ".pytest_cache", 63 | ".pytype", 64 | ".ruff_cache", 65 | ".svn", 66 | ".tox", 67 | ".venv", 68 | ".vscode", 69 | "__pypackages__", 70 | "_build", 71 | "buck-out", 72 | "build", 73 | "dist", 74 | "node_modules", 75 | "site-packages", 76 | "venv", 77 | ] 78 | 79 | # Same as Black. 80 | line-length = 80 81 | indent-width = 4 82 | 83 | # Assume Python 3.8 84 | target-version = "py38" 85 | 86 | [tool.ruff.lint] 87 | # Enable Pyflakes (`F`) and a subset of the pycodestyle (`E`) codes by default. 88 | # Unlike Flake8, Ruff doesn't enable pycodestyle warnings (`W`) or 89 | # McCabe complexity (`C901`) by default. 90 | select = ["D", "E101", "E4", "E7", "E9", "F", "I"] 91 | ignore = [] 92 | 93 | # Allow fix for all enabled rules (when `--fix`) is provided. 94 | fixable = ["ALL"] 95 | unfixable = [] 96 | 97 | # Allow unused variables when underscore-prefixed. 98 | dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$" 99 | 100 | [tool.ruff.format] 101 | # Like Black, use double quotes for strings. 102 | quote-style = "double" 103 | 104 | # Like Black, indent with spaces, rather than tabs. 105 | indent-style = "space" 106 | 107 | # Like Black, respect magic trailing commas. 108 | skip-magic-trailing-comma = false 109 | 110 | # Like Black, automatically detect the appropriate line ending. 111 | line-ending = "auto" 112 | 113 | # Use Google docstring formatting convention 114 | [tool.ruff.lint.pydocstyle] 115 | convention = "google" 116 | -------------------------------------------------------------------------------- /mkdocs_autoapi/generate_files/nav.py: -------------------------------------------------------------------------------- 1 | """Logic used to implement Nav class. 2 | 3 | As I work through the build, I'll update the documentation for this module. 4 | """ 5 | 6 | import dataclasses 7 | import os 8 | from typing import Iterable, Mapping, Optional, Tuple, Union 9 | 10 | 11 | class Nav: 12 | """MkDocs navigation data structure.""" 13 | 14 | _markdown_special_characters = tuple("!#()*+-[\\]_`{}") 15 | """The set of characters that need to be escaped in Markdown.""" 16 | 17 | def __init__(self): 18 | """Initialize a Nav object.""" 19 | self._data = dict() 20 | 21 | @dataclasses.dataclass 22 | class Item: 23 | """Define a navigation item.""" 24 | 25 | level: int 26 | """The item's nesting level. Starts at 0.""" 27 | title: str 28 | """The item's title.""" 29 | filename: Optional[str] 30 | """The path the item links to. If None, item is section, not link.""" 31 | 32 | def __setitem__(self, keys: Union[str, Tuple[str, ...]], value: str): 33 | """Add file link into the nav, under the sequence of titles. 34 | 35 | For example, writing `nav["Foo", "Bar"] = "foo/bar.md"` would mean 36 | creating a nav: 37 | `{"Foo": {"Bar": "foo/bar.md"}}`. 38 | 39 | Then, writing `nav["Foo", "Another"] = "test.md"` would merge with the 40 | existing sections where possible: 41 | `{"Foo": {"Bar": "foo/bar.md", "Another": "test.md"}}`. 42 | 43 | `keys` here can be any non-empty sequence of strings, it's just that 44 | Python implicitly creates a tuple from the comma-separated items in 45 | those square brackets. 46 | """ 47 | if isinstance(keys, str): 48 | keys = (keys,) 49 | cur = self._data 50 | if not keys: 51 | raise ValueError( 52 | f"Navigation path must not be empty (got {keys!r})" 53 | ) 54 | for key in keys: 55 | if not isinstance(key, str): 56 | message = f"Navigation path must consist of strings, but got a {type(key)}" # noqa: E501 57 | raise TypeError(message) 58 | if not key: 59 | raise ValueError( 60 | f"Navigation name parts must not be empty (got {keys!r})" 61 | ) 62 | cur = cur.setdefault(key, {}) 63 | cur[None] = os.fspath(value) 64 | 65 | def items(self) -> Iterable[Item]: 66 | """Allows viewing the nav as a flattened sequence.""" 67 | return self._items(self._data, 0) 68 | 69 | @classmethod 70 | def _items(cls, data: Mapping, level: int) -> Iterable[Item]: 71 | for key, value in data.items(): 72 | if key is not None: 73 | yield cls.Item(level=level, title=key, filename=value.get(None)) 74 | yield from cls._items(value, level + 1) 75 | 76 | def build_literate_nav(self, indentation: int = 0) -> Iterable[str]: 77 | """Build a sequence of lines for a literate navigation file. 78 | 79 | Steps: 80 | 1. For each item in the navigation: 81 | 1.1. Get the title and filename of the item. 82 | 1.2. Escape the title if it starts with a markdown escape 83 | character. 84 | 1.3. If the item has a filename, format it as a Markdown 85 | link. 86 | 1.4. Yield the formatted line. 87 | 88 | Args: 89 | indentation: 90 | The number of spaces to indent the whole nav. Useful when the 91 | nav is a part of a larger file. Defaults to 0. 92 | 93 | Yields: 94 | The lines of the navigation file. 95 | 96 | See Also: 97 | [mkdocs-literate-nav](https://github.com/oprypin/mkdocs-literate-nav) 98 | """ 99 | # Step 1 100 | for item in self.items(): 101 | # Step 1.1 102 | title = item.title 103 | file = item.filename 104 | 105 | # Step 1.2 106 | if title.startswith(self._markdown_special_characters): 107 | title = f"\\{title}" 108 | 109 | # Step 1.3 110 | if item.filename is not None: 111 | line = f"[{title}]({file})" 112 | else: 113 | line = title 114 | 115 | # Step 1.4 116 | indent = " " * (indentation + (4 * item.level)) 117 | yield f"{indent}* {line}\n" 118 | -------------------------------------------------------------------------------- /mkdocs_autoapi/generate_files/editor.py: -------------------------------------------------------------------------------- 1 | """File editor logic - copied from mkdocs-gen-files.""" 2 | 3 | # built-in imports 4 | import collections 5 | import os 6 | import os.path 7 | import pathlib 8 | import shutil 9 | from typing import IO, MutableMapping, Optional, Union 10 | 11 | # third-party imports 12 | from mkdocs.config import load_config 13 | from mkdocs.config.defaults import MkDocsConfig 14 | from mkdocs.structure.files import File, Files 15 | 16 | 17 | def file_sort_key(f: File): 18 | """Sort key for file.""" 19 | parts = pathlib.PurePosixPath(f.src_uri).parts 20 | return tuple( 21 | chr(f.name != "index" if i == len(parts) - 1 else 2) + p 22 | for i, p in enumerate(parts) 23 | ) 24 | 25 | 26 | class FilesEditor: 27 | """Context manager for editing files in a MkDocs build.""" 28 | 29 | config: MkDocsConfig 30 | """The current MkDocs [config](https://www.mkdocs.org/user-guide/plugins/#config).""" 31 | directory: str 32 | """The base directory for `open()` ([docs_dir](https://www.mkdocs.org/user-guide/configuration/#docs_dir)).""" 33 | edit_paths: MutableMapping[str, Union[str, None]] 34 | 35 | def open( 36 | self, name: str, mode, buffering=-1, encoding=None, *args, **kwargs 37 | ) -> IO: 38 | """Open a file under `docs_dir` virtually. 39 | 40 | This function, for all intents and purposes, is just an `open()` which 41 | pretends that it is running under [docs_dir](https://www.mkdocs.org/user-guide/configuration/#docs_dir). 42 | Write operations don't affect the actual files when running as part of 43 | an MkDocs build, but they do become part of the site build. 44 | """ 45 | path = self._get_file(name, new="w" in mode) 46 | if encoding is None and "b" not in mode: 47 | encoding = "utf-8" 48 | return open(path, mode, buffering, encoding, *args, **kwargs) 49 | 50 | def _get_file(self, name: str, new: bool = False) -> str: 51 | """Get file path for `name`, creating it if necessary.""" 52 | new_f = File( 53 | name, 54 | src_dir=self.directory, 55 | dest_dir=self.config.site_dir, 56 | use_directory_urls=self.config.use_directory_urls, 57 | ) 58 | new_f.generated_by = "mkdocs-gen-files" # type: ignore[attr-defined] 59 | normname = pathlib.PurePath(name).as_posix() 60 | 61 | if new or normname not in self._files: 62 | os.makedirs(os.path.dirname(new_f.abs_src_path), exist_ok=True) 63 | self._files[normname] = new_f 64 | self.edit_paths.setdefault(normname, None) 65 | return new_f.abs_src_path 66 | 67 | f = self._files[normname] 68 | if f.abs_src_path != new_f.abs_src_path: 69 | os.makedirs(os.path.dirname(new_f.abs_src_path), exist_ok=True) 70 | self._files[normname] = new_f 71 | self.edit_paths.setdefault(normname, None) 72 | shutil.copyfile(f.abs_src_path, new_f.abs_src_path) 73 | return new_f.abs_src_path 74 | 75 | return f.abs_src_path 76 | 77 | def set_edit_path(self, name: str, edit_name: Union[str, None]) -> None: 78 | """Choose a file path to use for the edit URI of this file.""" 79 | self.edit_paths[pathlib.PurePath(name).as_posix()] = edit_name and str( 80 | edit_name 81 | ) 82 | 83 | def __init__( 84 | self, 85 | files: Files, 86 | config: MkDocsConfig, 87 | directory: Optional[str] = None, 88 | ): 89 | """Initialize a FilesEditor object.""" 90 | self._files: MutableMapping[str, File] = collections.ChainMap( 91 | {}, {f.src_uri: f for f in files} 92 | ) 93 | self.config = config 94 | if directory is None: 95 | directory = config.docs_dir 96 | self.directory = directory 97 | self.edit_paths = {} 98 | 99 | _current = None 100 | _default = None 101 | 102 | @classmethod 103 | def current(cls): 104 | """Get FilesEditor instance for current MkDocs build. 105 | 106 | If used as part of a MkDocs build (*gen-files* plugin), it's an instance 107 | using virtual files that feed back into the build. 108 | 109 | If not, this still tries to load the MkDocs config to find out the 110 | `docs_dir`, and then actually performs any file writes that happen via 111 | `.open()`. 112 | 113 | Warning: 114 | This is global (not thread-safe). 115 | """ 116 | if cls._current: 117 | return cls._current 118 | if not cls._default: 119 | config = load_config("mkdocs.yml") 120 | config.plugins.run_event("config", config) 121 | cls._default = FilesEditor(Files([]), config) 122 | return cls._default 123 | 124 | def __enter__(self): 125 | """Set current instance to this one.""" 126 | type(self)._current = self 127 | return self 128 | 129 | def __exit__(self, *exc): 130 | """Clear current instance.""" 131 | type(self)._current = None 132 | 133 | @property 134 | def files(self) -> Files: 135 | """Access current file structure. 136 | 137 | [Files]: https://github.com/mkdocs/mkdocs/blob/master/mkdocs/structure/files.py 138 | """ 139 | files = sorted(self._files.values(), key=file_sort_key) 140 | return Files(files) 141 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # mkdocs-autoapi 2 | 3 | [](https://pypi.org/project/mkdocs-autoapi/) 4 | [](https://mkdocs-autoapi.readthedocs.io/en/latest/) 5 | [](https://github.com/astral-sh/ruff) 6 | 7 | 8 | ## Description 9 | 10 | `mkdocs-autoapi` is a MkDocs plugin that automatically generates API 11 | documentation from your project's source code. The idea for the plugin comes 12 | from this [recipe](https://mkdocstrings.github.io/recipes/#automatic-code-reference-pages) 13 | in the MkDocs documentation. 14 | 15 | ## Installation 16 | 17 | ### Requirements 18 | 19 | * Python version 3.6 or higher 20 | * MkDocs version 1.4.0 or higher 21 | * mkdocstrings version 0.19.0 or higher 22 | 23 | ### Installation via `pip` 24 | 25 | We recommend installing this package with `pip`: 26 | 27 | ```bash 28 | pip install mkdocs-autoapi 29 | ``` 30 | 31 | ## Usage 32 | 33 | ### Basic Usage 34 | 35 | To use the plugin, add the following configuration to your `mkdocs.yml` file: 36 | 37 | ```yaml 38 | plugins: 39 | - ... other plugin configuration ... 40 | - mkdocs-autoapi 41 | - mkdocstrings 42 | ``` 43 | 44 | ### Setting the Project Root 45 | 46 | By default, the plugin will use the current working directory as the project 47 | root. If you would like to use a different directory, you can specify a value 48 | in the `autoapi_dir` configuration option: 49 | 50 | ```yaml 51 | plugins: 52 | - ... other plugin configuration ... 53 | - mkdocs-autoapi: 54 | autoapi_dir: /path/to/autoapi/dir 55 | - mkdocstrings 56 | ``` 57 | 58 | ### Including and Ignoring Patterns 59 | 60 | You can ignore files and directories from the documentation by specifying a 61 | value in the `autoapi_ignore` configuration option. This option accepts a list 62 | of glob patterns. Note that the following patterns are always ignored: 63 | 64 | * `**/.venv/**/` 65 | * `**/venv/**/` 66 | 67 | Likewise, the `autoapi_file_patterns` configuration option allows for control of 68 | which files are included in the API reference. This option also accepts a list 69 | of glob patterns which are evaluated (recursively) relative to `autoapi_dir`. By 70 | default, all files with `.py` and `.pyi` extensions are included. 71 | 72 | As an example, suppose your project has the following structure: 73 | 74 | ```tree 75 | project/ 76 | docs/ 77 | index.md 78 | module/ 79 | __init__.py 80 | lorem.py 81 | ipsum.py 82 | dolor.py 83 | second_module/ 84 | __init__.py 85 | lorem.py 86 | sit.py 87 | amet.py 88 | venv/ 89 | mkdocs.yml 90 | README.md 91 | ``` 92 | 93 | To ignore all files named `lorem.py`, you can add the following configuration 94 | to your `mkdocs.yml` file: 95 | 96 | ```yaml 97 | plugins: 98 | - ... other plugin configuration ... 99 | - mkdocs-autoapi: 100 | autoapi_ignore: 101 | - "**/lorem.py" 102 | autoapi_file_patterns: 103 | - "*.py" 104 | - mkdocstrings 105 | ``` 106 | 107 | ## Disabling API Documentation Generation 108 | 109 | To disable API documentation generation, set the `autoapi_generate_api_docs` 110 | configuration option to `False`. This is useful when transitioning to manual 111 | documentation or when the API documentation is not needed. 112 | 113 | ## Including API Documentation in Navigation 114 | 115 | The inclusion of API documentation in the navigation can be controlled via the 116 | configuration option `autoapi_add_nav_entry`. This option accepts either a 117 | boolean value or a string. Behavior is as follows: 118 | 119 | * If `True`, then a section named "API Reference" will be added to the end of 120 | the navigation. 121 | * If `False`, then no section for the API documentation will be added. In this 122 | case, a manual link to the API documentation can be added to the navigation. 123 | * If a string, then a section with the specified name will be added to the end 124 | of the navigation. 125 | 126 | Example: To include the API documentation in the navigation under the section 127 | "Reference", add the following configuration to `mkdocs.yml`: 128 | 129 | ```yaml 130 | plugins: 131 | - ... other plugin configuration ... 132 | - mkdocs-autoapi: 133 | autoapi_add_nav_entry: Reference 134 | - mkdocstrings 135 | ``` 136 | 137 | Example: To disable the automatic addition of the API documentation to the 138 | navigation and add a manual link to the API documentation, add the following 139 | configuration to `mkdocs.yml`: 140 | 141 | ```yaml 142 | nav: 143 | - ... other navigation configuration ... 144 | - API: autoapi/ # target should be `autoapi_root` 145 | - ... other navigation configuration ... 146 | ``` 147 | 148 | More information on navigation configuration can be found in the 149 | [MkDocs User Guide](https://www.mkdocs.org/user-guide/configuration/#nav). 150 | 151 | ### Putting It All Together 152 | 153 | Again, consider the following project structure: 154 | 155 | ```tree 156 | project/ 157 | docs/ 158 | index.md 159 | module/ 160 | __init__.py 161 | lorem.py 162 | ipsum.py 163 | dolor.py 164 | second_module/ 165 | __init__.py 166 | lorem.py 167 | sit.py 168 | amet.py 169 | venv/ 170 | mkdocs.yml 171 | README.md 172 | ``` 173 | 174 | A full `mkdocs.yml` for the project might look like this: 175 | 176 | ```yaml mkdocs.yml 177 | site_name: Project 178 | 179 | nav: 180 | - Home: index.md 181 | - API Reference: autoapi/ 182 | 183 | plugins: 184 | - mkdocs-autoapi 185 | - mkdocstrings 186 | 187 | theme: 188 | name: readthedocs 189 | ``` 190 | 191 | More information MkDocs configuration through `mkdocs.yml` can be found in the 192 | [MkDocs User Guide](https://www.mkdocs.org/user-guide/configuration/). 193 | 194 | ## Contributing 195 | 196 | Contributions are always welcome! Please submit a pull request or open an issue 197 | to get started. 198 | 199 | ## License 200 | 201 | This project is licensed under the MIT License. See the [LICENSE](https://github.com/jcayers20/mkdocs-autoapi/blob/main/LICENSE) file 202 | for more information. 203 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | We greatly appreciate contributions to the project! Please read the guidelines 4 | below to get started. 5 | 6 | ## Issues and Discussions 7 | 8 | If you've found a bug, have a question, or have an idea for a new feature, 9 | please first check the [existing issues](https://github.com/jcayers20/mkdocs-autoapi/issues) 10 | to see if your issue has already been reported. If it hasn't, you can create a 11 | new issue by clicking the "New Issue" button on the issues page. The sections 12 | below provide guidance on what to include in your issue. 13 | 14 | ### Bug Reports 15 | 16 | When creating an issue for a bug, please include the following information: 17 | * A clear and descriptive title starting with "Bug: " 18 | * A detailed description of the bug 19 | * Steps to reproduce the bug (preferably with a minimal working example) 20 | 21 | ### Feature Requests 22 | 23 | When creating an issue for a feature request, please include the following 24 | information: 25 | * A clear and descriptive title starting with "Feature: " 26 | * A detailed description of the feature 27 | 28 | ### Questions 29 | 30 | If you have a question, please feel free to ask it in the [discussions](https://github.com/jcayers20/mkdocs-autoapi/discussions). 31 | We'll do our best to answer as soon as possible. If your question ends up being 32 | a bug or feature request, we'll work with you to create an issue for it. 33 | 34 | ## Setting Up a Local Development Environment 35 | 36 | ### Getting a Local Copy of the Project 37 | 38 | To get started, you'll need to fork the repository and clone it to your machine. 39 | GitHub has [a great guide](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/fork-a-repo?tool=webui) 40 | on how to do this. 41 | 42 | ### Creating a Virtual Environment 43 | 44 | We recommend using a virtual environment to keep your environment isolated for 45 | your work on this project. You can do this easily through the terminal. Note 46 | that the project is intended to work with Python 3.6 or later, so we recommend 47 | that you configure your interpreter to use Python 3.6. 48 | 49 | On Linux: 50 | ```bash 51 | $ python3 -m venv path/to/virtual_environment_directory # create 52 | $ source path/to/virtual_environment_directory/bin/activate # activate 53 | ``` 54 | 55 | On Windows: 56 | ```shell 57 | > python -m venv path\to\virtual_environment_directory # create 58 | > path\to\virtual_environment_directory\Scripts\activate # activate 59 | ``` 60 | 61 | Python IDE's generally have built-in support for virtual environments, so you 62 | can also set up a virtual environment through your IDE. Here are instructions 63 | for setting up a virtual environment in some popular IDE's: 64 | * [PyCharm](https://www.jetbrains.com/help/pycharm/creating-virtual-environment.html) 65 | * [VS Code](https://code.visualstudio.com/docs/python/environments) 66 | 67 | ### Installing Requirements 68 | 69 | Requirements are listed in `requirements.txt`. You can install them with pip: 70 | 71 | ```bash 72 | pip install -r requirements.txt 73 | ``` 74 | 75 | ### Enabling Pre-Commit Hooks 76 | 77 | We use pre-commit hooks to ensure that code is formatted consistently. Please be 78 | sure to enable these hooks before making changes. 79 | 80 | ```bash 81 | pre-commit install 82 | ``` 83 | 84 | Once enabled, the pre-commit hooks will run automatically when you attempt to 85 | commit changes. The first time you run the hooks, they may take a while to 86 | complete as the environment is set up. 87 | 88 | Note: Having `ruff` run on save will help you catch formatting problems before 89 | they are caught by the pre-commit hooks. You can enable this in your IDE. 90 | 91 | * [PyCharm](https://plugins.jetbrains.com/plugin/20574-ruff) 92 | * [VS Code](https://marketplace.visualstudio.com/items?itemName=charliermarsh.ruff) 93 | 94 | ### Creating a Working Branch 95 | 96 | Before you start making changes, you'll need to create a new branch to work in. 97 | This helps keep your changes isolated from the main branch and makes it easier 98 | to submit a pull request when you're done. 99 | 100 | ```bash 101 | git checkout -b your-branch-name [source-branch-name] 102 | ``` 103 | 104 | Note that `source-branch-name` is the branch you're branching from. For now, 105 | this will generally be `develop`. In the future, we will have versioned branches 106 | for the next bugfix release, the next minor release, and the next major release. 107 | Once we get there, you'll want to branch from the appropriate version branch. 108 | * Bug fixes: Next bugfix release branch 109 | * Features that are backwards-compatible: Next minor release branch 110 | * Features that are not backwards-compatible: Next major release branch 111 | 112 | ## Making Changes 113 | 114 | Once you've set up your local development environment, you're ready to start 115 | making changes. A couple of things to keep in mind as you make changes: 116 | 117 | 1. **Assign Yourself**: If you're working on an issue, please assign yourself to 118 | the issue so that others know you're working on it. 119 | 2. **Reference Issue(s)**: If your changes are related to an issue, be sure to 120 | reference that issue in your commit messages. This helps us track changes 121 | back to the issues they address. 122 | 123 | ## Submitting a Pull Request 124 | 125 | When you're ready to submit your changes, you'll need to create a [pull request](https://docs.github.com/en/pull-requests). 126 | to merge your changes into the appropriate branch. We'll review your changes, 127 | provide feedback, and work with you to get your changes merged. Here are a few 128 | things to keep in mind when submitting a pull request: 129 | 130 | 1. **Use a Descriptive Title**: Your pull request title should be descriptive 131 | and concise. It should give us a good idea of what your changes are about. 132 | 2. **Describe Your Changes**: Be sure to provide a clear description of the 133 | changes you've made and why you've made them. This helps us understand your 134 | changes. 135 | 3. **Reference Issues**: If your changes are related to an issue, be sure to 136 | reference that issue in your pull request. If your work completes an issue, then 137 | use closing keywords (e.g., `closes`, `fixes`, `resolves`) so that the issue 138 | will be automatically closed when the pull request is merged. 139 | -------------------------------------------------------------------------------- /mkdocs_autoapi/section_index/rewrite.py: -------------------------------------------------------------------------------- 1 | """Logic for rewriting the navigation data.""" 2 | 3 | # built-in imports 4 | import pathlib 5 | import textwrap 6 | from typing import Callable, Optional, Tuple, Union 7 | 8 | from jinja2 import BaseLoader, Environment 9 | 10 | __all__ = ["TemplateRewritingLoader"] 11 | 12 | 13 | class TemplateRewritingLoader(BaseLoader): 14 | """A Jinja2 template loader that rewrites certain templates.""" 15 | 16 | def __init__(self, loader: BaseLoader): 17 | """Initialize a TemplateRewritingLoader instance.""" 18 | self.loader = loader 19 | self.found_supported_theme = False 20 | 21 | def get_source( 22 | self, 23 | environment: Environment, 24 | template: str, 25 | ) -> Tuple[str, str, Union[Callable[[], bool], None]]: 26 | """Get the source of a template.""" 27 | src, filename, uptodate = self.loader.get_source(environment, template) 28 | old_src = src 29 | assert filename is not None 30 | path = pathlib.Path(filename).as_posix() 31 | 32 | if path.endswith("/mkdocs/templates/sitemap.xml"): 33 | src = _transform_mkdocs_sitemap_template(src) 34 | else: 35 | # the second path is used in MkDocs-Material >= 9.4 36 | if path.endswith( 37 | ( 38 | "/material/partials/nav-item.html", 39 | "/material/templates/partials/nav-item.html", 40 | ), 41 | ): 42 | src = _transform_material_nav_item_template(src) 43 | elif path.endswith( 44 | ( 45 | "/material/partials/tabs-item.html", 46 | "/material/templates/partials/tabs-item.html", 47 | ), 48 | ): 49 | src = _transform_material_tabs_item_template(src) 50 | elif path.endswith("/themes/readthedocs/base.html"): 51 | src = _transform_readthedocs_base_template(src) 52 | elif path.endswith("/nature/base.html"): 53 | src = None # Just works! 54 | else: 55 | return src, filename, uptodate 56 | self.found_supported_theme = True 57 | 58 | return src or old_src, filename, uptodate 59 | 60 | 61 | def _transform_mkdocs_sitemap_template(src: str) -> Union[str, None]: 62 | """Transform the sitemap template.""" 63 | if " in pages " in src: 64 | return None 65 | # The below only for versions <= 1.1.2. 66 | return src.replace( 67 | "{%- else %}", 68 | "{%- endif %}{% if item.url %}", 69 | ) 70 | 71 | 72 | def _transform_material_nav_item_template(src: str) -> str: 73 | """Transform the Material for MkDocs nav-item template.""" 74 | if "navigation.indexes" in src: 75 | return src.replace( 76 | "{% set indexes = [] %}", 77 | "{% set indexes = [nav_item] if nav_item.url else [] %}", 78 | ).replace( 79 | "{% if nav_item.children | length > 1 %}", 80 | "{% if nav_item.children %}", 81 | ) 82 | 83 | # The above only for versions >= 7.3, the below only for versions < 7.3. 84 | src = src.replace( 85 | "{% if nav_item.children %}", 86 | "{% if nav_item.children and not ('navigation.tabs' in features and level == 1 and not nav_item.active and nav_item.url) %}", # noqa: E501 87 | ) 88 | 89 | repl = """\ 90 | {% if nav_item.url %} 91 | 93 | {% endif %} 94 | [...] 95 | {% if nav_item.url %}{% endif %} 96 | """ # noqa: E501 97 | lines = src.split("\n") 98 | for i, (line1, line2) in enumerate(zip(lines, lines[1:])): 99 | for a, b in (line1, line2), (line2, line1): 100 | if "md-nav__icon" in a and b.endswith("{{ nav_item.title }}"): 101 | lines[i : i + 2] = (a, _replace_line(b, repl)) 102 | break 103 | return "\n".join(lines) 104 | 105 | 106 | def _transform_material_tabs_item_template(src: str) -> str: 107 | """Transform the Material for MkDocs tabs-item template.""" 108 | src = src.replace( 109 | "{% if first.children %}", "{% if first.children and not first.url %}" 110 | ) 111 | # The above only for versions >= 9.2 112 | src = src.replace( 113 | "{% if nav_item.children %}", 114 | "{% if nav_item.children and not nav_item.url %}", 115 | ) 116 | # The above only for versions > 6.1.7, the below only for versions <= 6.1.7. 117 | return src.replace( 118 | "(nav_item.children | first).url", 119 | "(nav_item.url or (nav_item.children | first).url)", 120 | ).replace( 121 | "if (nav_item.children | first).children", 122 | "if (nav_item.children | first).children and not nav_item.url", 123 | ) 124 | 125 | 126 | def _transform_readthedocs_base_template(src: str) -> Union[str, None]: 127 | """Transform the ReadTheDocs base template.""" 128 | if " if nav_item.is_page " in src: 129 | return None 130 | # The below only for versions < 1.6: 131 | repl = """\ 132 | {% if nav_item.url %} 133 |
140 | {% endif %} 141 | """ # noqa: E501 142 | lines = src.split("\n") 143 | for i, line in enumerate(lines): 144 | if "{{ nav_item.title }}" in line: 145 | lines[i] = _replace_line(lines[i], repl) 146 | return "\n".join(lines) 147 | 148 | 149 | def _replace_line( 150 | line: str, 151 | wrapper: str, 152 | new_line: Optional[str] = None, 153 | ) -> str: 154 | """Replace a line with a wrapper.""" 155 | leading_space = line[: -len(line.lstrip())] 156 | if new_line is None: 157 | new_line = line.lstrip() 158 | new_text = textwrap.dedent(wrapper.rstrip()).replace("[...]", new_line) 159 | return textwrap.indent(new_text, leading_space) 160 | -------------------------------------------------------------------------------- /docs/usage.md: -------------------------------------------------------------------------------- 1 | # Usage 2 | 3 | ## Setting the Project Root 4 | 5 | By default, the plugin considers the directory containing `mkdocs.yml` the 6 | project root directory. To use a different directory, specify the directory 7 | path in the `autoapi_dir` configuration option. The path can be absolute or 8 | relative to the directory containing `mkdocs.yml`. 9 | 10 | ```yaml 11 | plugins: 12 | - ... other plugin configuration ... 13 | - mkdocs-autoapi: 14 | autoapi_dir: path/to/autoapi/dir 15 | - mkdocstrings 16 | ``` 17 | 18 | A common use case for this option is projects using the 19 | [src layout](https://packaging.python.org/en/latest/discussions/src-layout-vs-flat-layout/). 20 | 21 | !!! note "Notes" 22 | 23 | If a directory containing `__init__.py` is specified, then the directory 24 | will be included in the relative path of its children. If not, then the 25 | directory will not be included. 26 | 27 | Be sure to include the `autoapi_dir` directory in the `paths` configuration 28 | for the `mkdocstrings` handler to ensure that the documentation is generated 29 | relative to the correct directory. If `autoapi_dir` does not contain 30 | `__init__.py`, then `autoapi_dir` must be included. If it does, then the 31 | parent directory of `autoapi_dir` must be included. For more information, 32 | see the `mkdocstrings` [documentation](https://mkdocstrings.github.io/python/usage/#using-the-paths-option). 33 | 34 | !!! example 35 | 36 | Consider a project with the following structure: 37 | 38 | ```tree 39 | project/ 40 | .venv/ 41 | ... 42 | docs/ 43 | index.md 44 | src/ 45 | awesome_package/ 46 | __init__.py 47 | module.py 48 | tools/ 49 | generate_awesomeness.py 50 | decrease_world_suck.py 51 | mkdocs.yml 52 | noxfile.py 53 | pyproject.toml 54 | README.md 55 | setup.py 56 | ``` 57 | 58 | For this project, it may or may not be desirable to include the `tools` 59 | directory in the API reference and we probably don't want to include 60 | the `*.py` files in the top-level directory. To ignore these items, we can 61 | set `autoapi_dir` to `src`: 62 | 63 | ```yaml title="mkdocs.yml" 64 | plugins: 65 | - ... other plugin configuration ... 66 | - mkdocs-autoapi: 67 | autoapi_dir: src # or /path/to/project/src 68 | - mkdocstrings: 69 | handlers: 70 | python: 71 | paths: 72 | - src 73 | ``` 74 | 75 | ## Including and Ignoring Patterns 76 | 77 | The `autoapi_ignore` configuration option allows for exclusion of files matching 78 | the specified pattern(s). This option accepts a list of [glob](https://man7.org/linux/man-pages/man7/glob.7.html) 79 | patterns. These patterns are evaluated relative to 80 | [autoapi_dir](#setting-the-project-root). 81 | 82 | Likewise, the `autoapi_file_patterns` configuration option allows for control of 83 | which files are included in the API reference. This option also accepts a list 84 | of glob patterns which are evaluated (recursively) relative to `autoapi_dir`. By 85 | default, all files with `.py` and `.pyi` extensions are included. 86 | 87 | !!! note 88 | The following patterns are commonly used for virtual environments and are 89 | always ignored: 90 | 91 | `venv/**/*`