pax_global_header 0000666 0000000 0000000 00000000064 13550071135 0014512 g ustar 00root root 0000000 0000000 52 comment=d566a301ce5ebcdeb3c3eeaf8702e40675353e0c mkautodoc-0.1.0/ 0000775 0000000 0000000 00000000000 13550071135 0013476 5 ustar 00root root 0000000 0000000 mkautodoc-0.1.0/.gitignore 0000664 0000000 0000000 00000000066 13550071135 0015470 0 ustar 00root root 0000000 0000000 *.pyc *.egg-info .coverage .pytest_cache htmlcov venv mkautodoc-0.1.0/.travis.yml 0000664 0000000 0000000 00000000227 13550071135 0015610 0 ustar 00root root 0000000 0000000 dist: xenial language: python cache: pip python: - "3.6" - "3.7" - "3.8-dev" install: - scripts/install script: - scripts/test mkautodoc-0.1.0/README.md 0000664 0000000 0000000 00000002355 13550071135 0014762 0 ustar 00root root 0000000 0000000 # MkAutoDoc Python API documention for MkDocs. This markdown extension adds `autodoc` style support, for use with MkDocs. ## Usage #### 1. Include the extension in you `mkdocs.yml` config file: ```yaml [...] markdown_extensions: - admonition - codehilite - mkautodoc ``` #### 2. Ensure the library you want to document is importable. This will depend on how your documentation building is setup, but you may need to use `pip install -e .` or modify `PYTHONPATH` in your docs build script. #### 3. Use the `:::` block syntax to add autodoc blocks to your documentation. ```markdown # API documentation ::: my_library.some_function :docstring: ::: my_library.SomeClass :docstring: :members: ``` ## Notes #### The :docstring: declaration. Renders the docstring of the associated function, method, or class. #### The `:members:` declaration. Renders documentation for member attributes of the associated class. Currently handles methods and properties. Instance attributes set during `__init__` are not currently recognised. May optionally accept a list of member attributes that should be documented. For example: ```markdown ::: my_library.SomeClass :docstring: :members: currency vat_registered calculate_expenses ``` mkautodoc-0.1.0/mkautodoc/ 0000775 0000000 0000000 00000000000 13550071135 0015464 5 ustar 00root root 0000000 0000000 mkautodoc-0.1.0/mkautodoc/__init__.py 0000664 0000000 0000000 00000000203 13550071135 0017570 0 ustar 00root root 0000000 0000000 from .extension import MKAutoDocExtension, makeExtension __version__ = "0.1.0" __all__ = ["MKAutoDocExtension", "makeExtension"] mkautodoc-0.1.0/mkautodoc/extension.py 0000664 0000000 0000000 00000020725 13550071135 0020060 0 ustar 00root root 0000000 0000000 from markdown import Markdown from markdown.extensions import Extension from markdown.blockprocessors import BlockProcessor from markdown.util import etree import importlib import inspect import re import typing # Fuzzy regex for determining source lines in __init__ that look like # attribute assignments. Eg. `self.counter = 0` SET_ATTRIBUTE = re.compile("^([ \t]*)self[.]([A-Za-z0-9_]+) *=") def import_from_string(import_str: str) -> typing.Any: module_str, _, attr_str = import_str.rpartition(".") try: module = importlib.import_module(module_str) except ImportError as exc: module_name = module_str.split(".", 1)[0] if exc.name != module_name: raise exc from None raise ValueError(f"Could not import module {module_str!r}.") try: return getattr(module, attr_str) except AttributeError as exc: raise ValueError(f"Attribute {attr_str!r} not found in module {module_str!r}.") def get_params(signature: inspect.Signature) -> typing.List[str]: """ Given a function signature, return a list of parameter strings to use in documentation. Eg. test(a, b=None, **kwargs) -> ['a', 'b=None', '**kwargs'] """ params = [] render_pos_only_separator = True render_kw_only_separator = True for parameter in signature.parameters.values(): value = parameter.name if parameter.default is not parameter.empty: value = f"{value}={parameter.default!r}" if parameter.kind is parameter.VAR_POSITIONAL: render_kw_only_separator = False value = f"*{value}" elif parameter.kind is parameter.VAR_KEYWORD: value = f"**{value}" elif parameter.kind is parameter.POSITIONAL_ONLY: if render_pos_only_separator: render_pos_only_separator = False params.append("/") elif parameter.kind is parameter.KEYWORD_ONLY: if render_kw_only_separator: render_kw_only_separator = False params.append("*") params.append(value) return params def last_iter(seq: typing.Sequence) -> typing.Iterator: """ Given an sequence, return a two-tuple (item, is_last) iterable. See: https://stackoverflow.com/a/1633483/596689 """ it = iter(seq) item = next(it) is_last = False for next_item in it: yield item, is_last item = next_item is_last = True yield item, is_last def trim_docstring(docstring: typing.Optional[str]) -> str: """ Trim leading indent from a docstring. See: https://www.python.org/dev/peps/pep-0257/#handling-docstring-indentation """ if not docstring: return "" # Convert tabs to spaces (following the normal Python rules) # and split into a list of lines: lines = docstring.expandtabs().splitlines() # Determine minimum indentation (first line doesn't count): indent = 1000 for line in lines[1:]: stripped = line.lstrip() if stripped: indent = min(indent, len(line) - len(stripped)) # Remove indentation (first line is special): trimmed = [lines[0].strip()] if indent < 1000: for line in lines[1:]: trimmed.append(line[indent:].rstrip()) # Strip off trailing and leading blank lines: while trimmed and not trimmed[-1]: trimmed.pop() while trimmed and not trimmed[0]: trimmed.pop(0) # Return a single string: return "\n".join(trimmed) class AutoDocProcessor(BlockProcessor): CLASSNAME = "autodoc" RE = re.compile(r"(?:^|\n)::: ?([:a-zA-Z0-9_.]*) *(?:\n|$)") RE_SPACES = re.compile(" +") def __init__(self, parser, md=None): super().__init__(parser=parser) self.md = md def test(self, parent: etree.Element, block: etree.Element) -> bool: sibling = self.lastChild(parent) return bool( self.RE.search(block) or ( block.startswith(" " * self.tab_length) and sibling is not None and sibling.get("class", "").find(self.CLASSNAME) != -1 ) ) def run(self, parent: etree.Element, blocks: etree.Element) -> None: sibling = self.lastChild(parent) block = blocks.pop(0) m = self.RE.search(block) if m: block = block[m.end() :] # removes the first line block, theRest = self.detab(block) if m: import_string = m.group(1) item = import_from_string(import_string) autodoc_div = etree.SubElement(parent, "div") autodoc_div.set("class", self.CLASSNAME) self.render_signature(autodoc_div, item, import_string) for line in block.splitlines(): if line.startswith(":docstring:"): docstring = trim_docstring(item.__doc__) self.render_docstring(autodoc_div, item, docstring) elif line.startswith(":members:"): members = line.split()[1:] or None self.render_members(autodoc_div, item, members=members) if theRest: # This block contained unindented line(s) after the first indented # line. Insert these lines as the first block of the master blocks # list for future processing. blocks.insert(0, theRest) def render_signature( self, elem: etree.Element, item: typing.Any, import_string: str ) -> None: module_string, _, name_string = import_string.rpartition(".") # Eg: `some_module.attribute_name` signature_elem = etree.SubElement(elem, "div") signature_elem.set("class", "autodoc-signature") if inspect.isclass(item): qualifier_elem = etree.SubElement(signature_elem, "em") qualifier_elem.text = "class " name_elem = etree.SubElement(signature_elem, "code") if module_string: name_elem.text = module_string + "." main_name_elem = etree.SubElement(name_elem, "strong") main_name_elem.text = name_string # If this is a property, then we're done. if not callable(item): return # Eg: `(a, b='default', **kwargs)`` signature = inspect.signature(item) bracket_elem = etree.SubElement(signature_elem, "span") bracket_elem.text = "(" bracket_elem.set("class", "autodoc-punctuation") if signature.parameters: for param, is_last in last_iter(get_params(signature)): param_elem = etree.SubElement(signature_elem, "em") param_elem.text = param param_elem.set("class", "autodoc-param") if not is_last: comma_elem = etree.SubElement(signature_elem, "span") comma_elem.text = ", " comma_elem.set("class", "autodoc-punctuation") bracket_elem = etree.SubElement(signature_elem, "span") bracket_elem.text = ")" bracket_elem.set("class", "autodoc-punctuation") def render_docstring( self, elem: etree.Element, item: typing.Any, docstring: str ) -> None: docstring_elem = etree.SubElement(elem, "div") docstring_elem.set("class", "autodoc-docstring") md = Markdown(extensions=self.md.registeredExtensions) docstring_elem.text = md.convert(docstring) def render_members( self, elem: etree.Element, item: typing.Any, members: typing.List[str] = None ) -> None: members_elem = etree.SubElement(elem, "div") members_elem.set("class", "autodoc-members") if members is None: members = sorted([attr for attr in dir(item) if not attr.startswith("_")]) info_items = [] for attribute_name in members: attribute = getattr(item, attribute_name) docs = trim_docstring(getattr(attribute, "__doc__", "")) info = (attribute_name, docs) info_items.append(info) for attribute_name, docs in info_items: attribute = getattr(item, attribute_name) self.render_signature(members_elem, attribute, attribute_name) self.render_docstring(members_elem, attribute, docs) class MKAutoDocExtension(Extension): def extendMarkdown(self, md: Markdown) -> None: md.registerExtension(self) processor = AutoDocProcessor(md.parser, md=md) md.parser.blockprocessors.register(processor, "mkautodoc", 110) def makeExtension(): return MKAutoDocExtension() mkautodoc-0.1.0/requirements.txt 0000664 0000000 0000000 00000000054 13550071135 0016761 0 ustar 00root root 0000000 0000000 markdown # Testing black pytest pytest-cov mkautodoc-0.1.0/scripts/ 0000775 0000000 0000000 00000000000 13550071135 0015165 5 ustar 00root root 0000000 0000000 mkautodoc-0.1.0/scripts/clean 0000775 0000000 0000000 00000000350 13550071135 0016173 0 ustar 00root root 0000000 0000000 #!/bin/sh -ex PROJECT=mkautodoc find ${PROJECT} -type f -name "*.py[co]" -delete find ${PROJECT} -type d -name __pycache__ -delete find tests -type d -name __pycache__ -delete rm -rf dist htmlcov .pytest_cache ${PROJECT}.egg-info mkautodoc-0.1.0/scripts/install 0000775 0000000 0000000 00000000342 13550071135 0016560 0 ustar 00root root 0000000 0000000 #!/bin/sh -ex if [ "${CONTINUOUS_INTEGRATION}" = "true" ]; then BIN_PATH="" else rm -rf venv python -m venv venv BIN_PATH="venv/bin/" fi ${BIN_PATH}pip install -r requirements.txt ${BIN_PATH}pip install -e . mkautodoc-0.1.0/scripts/lint 0000775 0000000 0000000 00000000226 13550071135 0016061 0 ustar 00root root 0000000 0000000 #!/bin/sh -ex PROJECT="mkautodoc" if [ -d "venv" ]; then BIN_PATH="venv/bin/" else BIN_PATH="" fi ${BIN_PATH}black ${PROJECT} tests "${@}" mkautodoc-0.1.0/scripts/publish 0000775 0000000 0000000 00000001207 13550071135 0016561 0 ustar 00root root 0000000 0000000 #!/bin/sh -ex PROJECT="mkautodoc" VERSION=`cat ${PROJECT}/__init__.py | grep __version__ | sed "s/__version__ = //" | sed "s/'//g"` if [ -d 'venv' ] ; then BIN_PATH="venv/bin/" else BIN_PATH="" fi if ! command -v "${BIN_PATH}twine" &>/dev/null ; then echo "Unable to find the 'twine' command." echo "Install from PyPI, using '${BIN_PATH}pip install twine'." exit 1 fi scripts/clean ${BIN_PATH}python setup.py sdist ${BIN_PATH}twine upload dist/* # ${BIN_PATH}mkdocs gh-deploy scripts/clean echo "You probably want to also tag the version now:" echo "git tag -a ${VERSION} -m 'version ${VERSION}'" echo "git push --tags" mkautodoc-0.1.0/scripts/test 0000775 0000000 0000000 00000000451 13550071135 0016072 0 ustar 00root root 0000000 0000000 #!/bin/sh -ex PROJECT="mkautodoc" export PYTHONPATH=tests/mocklib if [ -d 'venv' ] ; then BIN_PATH="venv/bin/" else BIN_PATH="" fi scripts/lint --check ${BIN_PATH}pytest tests --cov=${PROJECT} --cov=tests --cov-report= ${BIN_PATH}coverage html ${BIN_PATH}coverage report --show-missing mkautodoc-0.1.0/setup.py 0000664 0000000 0000000 00000002524 13550071135 0015213 0 ustar 00root root 0000000 0000000 #!/usr/bin/env python # -*- coding: utf-8 -*- import os import re import sys from setuptools import setup def get_version(package): """ Return package version as listed in `__version__` in `init.py`. """ init_py = open(os.path.join(package, '__init__.py')).read() return re.search("__version__ = ['\"]([^'\"]+)['\"]", init_py).group(1) def get_packages(package): """ Return root package and all sub-packages. """ return [dirpath for dirpath, dirnames, filenames in os.walk(package) if os.path.exists(os.path.join(dirpath, '__init__.py'))] version = get_version('mkautodoc') setup( name='mkautodoc', version=version, url='https://github.com/encode/mkautodoc', license='BSD', description='AutoDoc for MarkDown', author='Tom Christie', author_email='tom@tomchristie.com', packages=get_packages('mkautodoc'), install_requires=[], python_requires='>=3.6', classifiers=[ 'Development Status :: 3 - Alpha', 'Intended Audience :: Developers', 'License :: OSI Approved :: BSD License', 'Operating System :: OS Independent', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', ], ) mkautodoc-0.1.0/tests/ 0000775 0000000 0000000 00000000000 13550071135 0014640 5 ustar 00root root 0000000 0000000 mkautodoc-0.1.0/tests/mocklib/ 0000775 0000000 0000000 00000000000 13550071135 0016260 5 ustar 00root root 0000000 0000000 mkautodoc-0.1.0/tests/mocklib/mocklib.py 0000664 0000000 0000000 00000001001 13550071135 0020242 0 ustar 00root root 0000000 0000000 def example_function(a, b=None, *args, **kwargs): """ This is a function with a *docstring*. """ class ExampleClass: """ This is a class with a *docstring*. """ def __init__(self): """ This is an __init__ with a *docstring*. """ def example_method(self, a, b=None): """ This is a method with a *docstring*. """ @property def example_property(self): """ This is a property with a *docstring*. """ mkautodoc-0.1.0/tests/test_extension.py 0000664 0000000 0000000 00000004204 13550071135 0020265 0 ustar 00root root 0000000 0000000 import markdown def test_docstring(): content = """ # Example ::: mocklib.example_function :docstring: """ output = markdown.markdown(content, extensions=["mkautodoc"]) assert output.splitlines() == [ "
mocklib.example_function
(a, b=None, *args, **kwargs)This is a function with a docstring.
mocklib.ExampleClass
()This is a class with a docstring.
example_method
(self, a, b=None)This is a method with a docstring.
example_property
This is a property with a docstring.