├── MANIFEST.in ├── setup.py ├── mkdocs_autodoc ├── autodoc.jinja2 ├── __init__.py └── autodoc.py ├── LICENSE ├── .gitignore ├── magicpatch.py └── README.md /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include mkdocs_autodoc/autodoc.jinja2 2 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | setup( 4 | name="mkdocs-autodoc", 5 | version="0.1.2", 6 | url="https://github.com/restaction/mkdocs-autodoc", 7 | license="MIT", 8 | description="Auto generate API document in MKDocs", 9 | author="guyskk", 10 | author_email="guyskk@qq.com", 11 | keywords=["mkdocs"], 12 | packages=["mkdocs_autodoc"], 13 | py_modules=["magicpatch"], 14 | package_data={ 15 | "mkdocs_autodoc": ["autodoc.jinja2"] 16 | }, 17 | include_package_data=True, 18 | entry_points={ 19 | "mkdocs.themes": [ 20 | "autodoc = mkdocs_autodoc", 21 | ] 22 | }, 23 | zip_safe=False 24 | ) 25 | -------------------------------------------------------------------------------- /mkdocs_autodoc/autodoc.jinja2: -------------------------------------------------------------------------------- 1 | 4 | {% for item in contents %} 5 |

{{ item["title"] }}

6 |
7 |
8 | {{ item["signature"] }} 9 |
10 |
11 | {{ item["desc"] }} 12 | {% for key in item["meta"] %} 13 | {% if item["meta"][key] %} 14 |
15 |
{{ key }}:
16 |
{{ item["meta"][key] }}
17 |
18 | {% endif %} 19 | {% endfor %} 20 | {% for routine in item["routines"] %} 21 |
22 |
23 | {{ routine["signature"] }} 24 |
25 |
26 |

{{ routine["title"] }}

27 | {{ routine["desc"] }} 28 | {% for key in routine["meta"] %} 29 | {% if routine["meta"][key] %} 30 |
31 |
{{ key }}:
32 |
{{ routine["meta"][key] }}
33 |
34 | {% endif %} 35 | {% endfor %} 36 |
37 |
38 | {% endfor %} 39 |
40 |
41 | {% endfor %} 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 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 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | .hypothesis/ 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Django stuff: 53 | *.log 54 | local_settings.py 55 | 56 | # Flask stuff: 57 | instance/ 58 | .webassets-cache 59 | 60 | # Scrapy stuff: 61 | .scrapy 62 | 63 | # Sphinx documentation 64 | docs/_build/ 65 | 66 | # PyBuilder 67 | target/ 68 | 69 | # IPython Notebook 70 | .ipynb_checkpoints 71 | 72 | # pyenv 73 | .python-version 74 | 75 | # celery beat schedule file 76 | celerybeat-schedule 77 | 78 | # dotenv 79 | .env 80 | 81 | # virtualenv 82 | venv/ 83 | ENV/ 84 | 85 | # Spyder project settings 86 | .spyderproject 87 | 88 | # Rope project settings 89 | .ropeproject 90 | -------------------------------------------------------------------------------- /magicpatch.py: -------------------------------------------------------------------------------- 1 | """ 2 | Patch functions in magic way 3 | 4 | Usage: 5 | 6 | >>> from magicpatch import patch 7 | >>> def func(x): 8 | ... return x 9 | ... 10 | >>> @patch(func) 11 | ... def patch_func(be_patched, x): 12 | ... return 2*be_patched(x) 13 | ... 14 | >>> func(1) 15 | 2 16 | >>> 17 | """ 18 | import uuid 19 | import types 20 | import functools 21 | 22 | 23 | def copy_func(f): 24 | """ 25 | Deep copy function of Python3. 26 | 27 | Based on: 28 | http://stackoverflow.com/a/6528148/190597 (Glenn Maynard) 29 | http://stackoverflow.com/questions/13503079/how-to-create-a-copy-of-a-python-function (Aaron Hall) 30 | """ # noqa 31 | g = types.FunctionType( 32 | f.__code__, f.__globals__, f.__name__, 33 | f.__defaults__, f.__closure__ 34 | ) 35 | g = functools.update_wrapper(g, f) 36 | g.__kwdefaults__ = f.__kwdefaults__ 37 | return g 38 | 39 | 40 | PATCHED = {} 41 | 42 | 43 | def patch(f_be_patched): 44 | def decorater(f): 45 | key = str(uuid.uuid4()) 46 | PATCHED[key] = functools.partial(f, copy_func(f_be_patched)) 47 | code = """ 48 | def wrapper(*args, **kwargs): 49 | return __import__("magicpatch").PATCHED["{}"](*args, **kwargs) 50 | """.format(key) 51 | context = {} 52 | exec(code, context) 53 | f_be_patched.__code__ = context["wrapper"].__code__ 54 | return f 55 | return decorater 56 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MKDocs-Autodoc 2 | 3 | Auto generate API document in MKDocs, just like autodoc for Sphinx. 4 | 5 | ## Install 6 | 7 | pip install git+https://github.com/restaction/mkdocs-autodoc.git 8 | 9 | After install this plugin, it will auto patch mkdocs and disable --theme option 10 | of mkdocs cli in order to use custom theme in ReadTheDocs. 11 | 12 | See issues below if you wonder **Why should I patch**: 13 | https://github.com/rtfd/readthedocs.org/issues/978 14 | https://github.com/mkdocs/mkdocs/issues/206 15 | 16 | ## Usage 17 | 18 | Write a *.autodoc file, which contains which should be rendered in your page. 19 | 20 | The syntax is: 21 | 22 | module 23 | package.module 24 | module::ClassName 25 | package.module::ClassName 26 | 27 | Each line contains a module or class, use `::` to split module and class. 28 | This plugin will render functions which defined in the module you selected, 29 | or methods of the class you selected, built-in, private, speciall 30 | functions/methods will be ignored. 31 | 32 | This plugin works well for 33 | [Google](https://google.github.io/styleguide/pyguide.html#Comments) style docstrings, 34 | it not works well for [NumPy](https://github.com/numpy/numpy/blob/master/doc/HOWTO_DOCUMENT.rst.txt) and [Sphinx](http://www.sphinx-doc.org/en/stable/ext/autodoc.html) style docstrings currently. 35 | 36 | Then add the *.autodoc file(eg: api.autodoc) to mkdocs.yml: 37 | 38 | site_name: AutodocDemo 39 | pages: 40 | - Home: index.md 41 | - API: api.autodoc 42 | 43 | Save and restart `mkdocs serve`, it will works. 44 | 45 | ## Who use it 46 | 47 | [Flask-Restaction简体中文文档](https://github.com/restaction/docs-zh_CN) 48 | [Flask-Restaction English document](https://github.com/restaction/docs-en) 49 | -------------------------------------------------------------------------------- /mkdocs_autodoc/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | MKDocs-Autodoc 3 | 4 | This plugin implemented autodoc in MKDocs, just like autodoc in Sphinx. 5 | Doc strings should follow Google Python Style, otherwise will not well parsed. 6 | """ 7 | import io 8 | import os 9 | from mkdocs import utils 10 | from mkdocs.config import load_config 11 | from mkdocs.commands import build 12 | from mkdocs.commands.build import log, get_global_context, get_page_context 13 | from mkdocs.toc import TableOfContents, AnchorLink 14 | from mkdocs_autodoc.autodoc import parse_selected 15 | from magicpatch import patch 16 | 17 | AUTODOC_MARK = ".autodoc" 18 | TEMPLATE_PATH = os.path.join(os.path.dirname(__file__), "autodoc.jinja2") 19 | 20 | 21 | def create_toc(titles): 22 | """ 23 | Create table of contents 24 | """ 25 | toc = TableOfContents("") 26 | if not titles: 27 | return toc 28 | first = titles.pop(0) 29 | link = AnchorLink(title=first, url="#0") 30 | link.active = True 31 | link.children = [ 32 | AnchorLink(title=title, url="#%d" % (index + 1)) 33 | for index, title in enumerate(titles) 34 | ] 35 | toc.items = [link] 36 | return toc 37 | 38 | 39 | def get_complete_paths(config, page): 40 | """ 41 | Return the complete input/output paths for the supplied page. 42 | """ 43 | input_path = os.path.join(config['docs_dir'], page.input_path) 44 | output_path = os.path.join(config['site_dir'], page.output_path) 45 | return input_path, output_path 46 | 47 | 48 | def build_autodoc(page, config, site_navigation, env, dump_json, dirty=False): 49 | """ 50 | Build autodoc, just like mkdocs.commands.build._build_page 51 | """ 52 | input_path, output_path = get_complete_paths(config, page) 53 | try: 54 | input_content = io.open(input_path, 'r', encoding='utf-8').read() 55 | except IOError: 56 | log.error('file not found: %s', input_path) 57 | raise 58 | # render autodoc contents 59 | tmplstr = io.open(TEMPLATE_PATH).read() 60 | template = env.from_string(tmplstr) 61 | contents, titles = parse_selected(input_content) 62 | table_of_contents = create_toc(titles) 63 | html_content = template.render(contents=contents) 64 | # render page 65 | meta = None 66 | context = get_global_context(site_navigation, config) 67 | context.update(get_page_context( 68 | page, html_content, table_of_contents, meta, config 69 | )) 70 | template = env.get_template('base.html') 71 | output_content = template.render(context) 72 | utils.write_file(output_content.encode('utf-8'), output_path) 73 | return html_content, table_of_contents, None 74 | 75 | 76 | @patch(build._build_page) 77 | def build_page(f, page, *args, **kwargs): 78 | """ 79 | A patch of mkdocs.commands.build._build_page 80 | """ 81 | if page.input_path.endswith(AUTODOC_MARK): 82 | return build_autodoc(page, *args, **kwargs) 83 | return f(page, *args, **kwargs) 84 | 85 | 86 | @patch(build.build) 87 | def patched_build(f, config, *args, **kwargs): 88 | print("HACK".center(60, "-")) 89 | real_config = load_config(config_file=None) 90 | for k in ["theme", "theme_dir"]: 91 | config[k] = real_config[k] 92 | return f(config, *args, **kwargs) 93 | -------------------------------------------------------------------------------- /mkdocs_autodoc/autodoc.py: -------------------------------------------------------------------------------- 1 | """ 2 | Autodoc 3 | 4 | Some useful functions for parsing doc string of modules. 5 | """ 6 | import importlib 7 | import inspect 8 | import pydoc 9 | from markdown import markdown 10 | 11 | EXTENSIONS = ['nl2br', 'tables', 'fenced_code'] 12 | DOC_STRING_MARKS = ["Args", "Returns", "Yields", "Raises", "Attributes"] 13 | 14 | 15 | def load_selected(text): 16 | """ 17 | Load selected module or class in text 18 | 19 | text syntax: 20 | 21 | module 22 | package.module 23 | module::class 24 | 25 | Returns: 26 | A list of loaded objects 27 | """ 28 | result = [] 29 | for line in text.splitlines(): 30 | if not line: 31 | continue 32 | if "::" in line: 33 | module, classname = line.rsplit("::", maxsplit=1) 34 | module = importlib.import_module(module) 35 | result.append(getattr(module, classname)) 36 | else: 37 | result.append(importlib.import_module(line)) 38 | return result 39 | 40 | 41 | def parse_meta(meta): 42 | """ 43 | Parse returns meta of split_doc 44 | 45 | Returns: 46 | A dict of parsed meta 47 | """ 48 | parsed = {} 49 | for section in meta: 50 | mark, content = section.split("\n", maxsplit=1) 51 | mark = mark.strip("\n:") 52 | parsed[mark] = markdown( 53 | inspect.cleandoc(content), extensions=EXTENSIONS) 54 | return parsed 55 | 56 | 57 | def split_doc(doc): 58 | """ 59 | Split docstring into title, desc and meta 60 | 61 | Returns: 62 | A tuple of title, desc, meta. 63 | 64 | title: the summary line of doc 65 | desc: the description part 66 | meta: a list of sections of the meta part 67 | """ 68 | if not doc: 69 | return "", "", [] 70 | # split title/desc,meta 71 | lines = doc.strip().split("\n", maxsplit=1) 72 | if len(lines) == 1: 73 | return lines[0], "", [] 74 | title, doc = lines 75 | # split desc/meta 76 | indexs = [] 77 | for mark in DOC_STRING_MARKS: 78 | i = doc.find("%s:" % mark) 79 | if i >= 0: 80 | indexs.append(i) 81 | if not indexs: 82 | return title, doc, [] 83 | indexs = sorted(indexs) 84 | desc = doc[:indexs[0]] 85 | # split meta into sections 86 | sections = [] 87 | for i, j in zip(indexs[:-1], indexs[1:]): 88 | sections.append(doc[i:j]) 89 | sections.append(doc[indexs[-1]:]) 90 | return title, desc, sections 91 | 92 | 93 | def parse_doc(doc): 94 | """ 95 | Parse docstring 96 | 97 | Returns: 98 | A tuple of title, desc, meta. 99 | """ 100 | title, desc, meta = split_doc(doc) 101 | desc = markdown(desc, extensions=EXTENSIONS) 102 | meta = parse_meta(meta) 103 | return title, desc, meta 104 | 105 | 106 | def get_signature(obj): 107 | """ 108 | Get signature of module/class/routine 109 | 110 | Returns: 111 | A string signature 112 | """ 113 | name = obj.__name__ 114 | if inspect.isclass(obj): 115 | if hasattr(obj, "__init__"): 116 | signature = str(inspect.signature(obj.__init__)) 117 | return "class %s%s" % (name, signature) 118 | else: 119 | signature = "%s()" % name 120 | elif inspect.ismodule(obj): 121 | signature = name 122 | else: 123 | signature = str(inspect.signature(obj)) 124 | return name + signature 125 | return signature 126 | 127 | 128 | def parse_routine(obj): 129 | """ 130 | Parse routine object 131 | 132 | Returns: 133 | A dict, eg: 134 | 135 | { 136 | "signature": "func(*args, **kwargs)", 137 | "title": "title", 138 | "desc": "desc", 139 | "meta": meta 140 | } 141 | """ 142 | title, desc, meta = parse_doc(inspect.getdoc(obj)) 143 | return { 144 | "signature": get_signature(obj), 145 | "title": title, 146 | "desc": desc, 147 | "meta": meta 148 | } 149 | 150 | 151 | def parse_module_or_class(obj): 152 | """ 153 | Parse module or class and routines in it. 154 | 155 | Returns: 156 | A dics, eg: 157 | 158 | { 159 | "routines": routines, 160 | "signature": signature, 161 | "title": title, 162 | "desc": desc, 163 | "meta": meta 164 | } 165 | """ 166 | def predicate(x): 167 | # exclude not routines 168 | if not inspect.isroutine(x): 169 | return False 170 | # exclude private and special 171 | if x.__name__.startswith("_"): 172 | return False 173 | # exclude routines not defined in the module 174 | if inspect.ismodule(obj) and inspect.getmodule(x) != obj: 175 | return False 176 | return True 177 | routines = inspect.getmembers(obj, predicate) 178 | title, desc, meta = parse_doc(inspect.getdoc(obj)) 179 | parsed = [parse_routine(obj) for name, obj in routines] 180 | return { 181 | "routines": parsed, 182 | "signature": get_signature(obj), 183 | "title": title, 184 | "desc": desc, 185 | "meta": meta 186 | } 187 | 188 | 189 | def get_name(obj): 190 | """ 191 | Get the name of object 192 | """ 193 | if inspect.isclass(obj): 194 | name = pydoc.classname(obj, None) 195 | name = obj.__name__ 196 | return name.rsplit(".", maxsplit=1)[-1] 197 | 198 | 199 | def parse_selected(text): 200 | """ 201 | Parse selected module and class 202 | 203 | Returns: 204 | tuple(contents, titles) 205 | """ 206 | titles = [] 207 | contents = [] 208 | for obj in load_selected(text): 209 | titles.append(get_name(obj)) 210 | item = parse_module_or_class(obj) 211 | contents.append(item) 212 | return contents, titles 213 | --------------------------------------------------------------------------------