├── 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 |
--------------------------------------------------------------------------------