pax_global_header 0000666 0000000 0000000 00000000064 14272435345 0014523 g ustar 00root root 0000000 0000000 52 comment=ad41abc4a94c4d63347e808ce6ca1dc0fb800bfb mkautodoc-0.2.0/ 0000775 0000000 0000000 00000000000 14272435345 0013510 5 ustar 00root root 0000000 0000000 mkautodoc-0.2.0/.gitignore 0000664 0000000 0000000 00000000066 14272435345 0015502 0 ustar 00root root 0000000 0000000 *.pyc *.egg-info .coverage .pytest_cache htmlcov venv mkautodoc-0.2.0/.travis.yml 0000664 0000000 0000000 00000000227 14272435345 0015622 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.2.0/README.md 0000664 0000000 0000000 00000003373 14272435345 0014775 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: ``` #### 4. Optionally, add styling for the API docs Update your `mkdocs.yml` to include some custom CSS. ```yaml [...] extra_css: - css/custom.css ``` Then add a `css/custom.css` file to your documentation. ```css div.autodoc-docstring { padding-left: 20px; margin-bottom: 30px; border-left: 5px solid rgba(230, 230, 230); } div.autodoc-members { padding-left: 20px; margin-bottom: 15px; } ``` ## 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.2.0/mkautodoc/ 0000775 0000000 0000000 00000000000 14272435345 0015476 5 ustar 00root root 0000000 0000000 mkautodoc-0.2.0/mkautodoc/__init__.py 0000664 0000000 0000000 00000000203 14272435345 0017602 0 ustar 00root root 0000000 0000000 from .extension import MKAutoDocExtension, makeExtension __version__ = "0.2.0" __all__ = ["MKAutoDocExtension", "makeExtension"] mkautodoc-0.2.0/mkautodoc/extension.py 0000664 0000000 0000000 00000021176 14272435345 0020073 0 ustar 00root root 0000000 0000000 from markdown import Markdown from markdown.extensions import Extension from markdown.blockprocessors import BlockProcessor from xml.etree import ElementTree as 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 a 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 " elif inspect.iscoroutinefunction(item): qualifier_elem = etree.SubElement(signature_elem, "em") qualifier_elem.text = "async " 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.2.0/requirements.txt 0000664 0000000 0000000 00000000050 14272435345 0016767 0 ustar 00root root 0000000 0000000 -e . # Testing black pytest pytest-cov mkautodoc-0.2.0/scripts/ 0000775 0000000 0000000 00000000000 14272435345 0015177 5 ustar 00root root 0000000 0000000 mkautodoc-0.2.0/scripts/clean 0000775 0000000 0000000 00000000350 14272435345 0016205 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.2.0/scripts/install 0000775 0000000 0000000 00000000407 14272435345 0016574 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 --upgrade pip ${BIN_PATH}pip install -r requirements.txt ${BIN_PATH}pip install -e . mkautodoc-0.2.0/scripts/lint 0000775 0000000 0000000 00000000226 14272435345 0016073 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.2.0/scripts/publish 0000775 0000000 0000000 00000001207 14272435345 0016573 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.2.0/scripts/test 0000775 0000000 0000000 00000000451 14272435345 0016104 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.2.0/setup.py 0000664 0000000 0000000 00000002543 14272435345 0015226 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/tomchristie/mkautodoc', license='BSD', description='AutoDoc for MarkDown', author='Tom Christie', author_email='tom@tomchristie.com', packages=get_packages('mkautodoc'), install_requires=["Markdown"], 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.2.0/tests/ 0000775 0000000 0000000 00000000000 14272435345 0014652 5 ustar 00root root 0000000 0000000 mkautodoc-0.2.0/tests/__init__.py 0000664 0000000 0000000 00000000000 14272435345 0016751 0 ustar 00root root 0000000 0000000 mkautodoc-0.2.0/tests/assertions.py 0000664 0000000 0000000 00000004651 14272435345 0017424 0 ustar 00root root 0000000 0000000 from xml import etree from xml.dom import minidom import textwrap def assert_xml_equal(xml_string, expected_xml_string): """ Assert equality of two xml strings, particularly that the contents of each string have the same elements, with the same attributes (e.g. class, text) and the same non-xml string contents """ # this prints a human-formatted string of what the test passed in -- useful # if you need to modify test expectations after you've modified # a rendering and tested it visually print(to_readable_error_output(xml_string)) assert_elements_equal( etree.ElementTree.fromstring(tostring(xml_string)), etree.ElementTree.fromstring(tostring(expected_xml_string)), ) def assert_elements_equal(element, reference_element): """ Assert, recursively, the equality of two etree objects. """ assert ( element.text == reference_element.text ), f"Text doesn't match: {element.text} =/= {reference_element.text}." assert ( element.attrib == reference_element.attrib ), f"Attrib doesn't match: {element.attrib} =/= {reference_element.attrib}" assert len(element) == len( reference_element ), f"Expected {len(reference_element)} children but got {len(element)}" for sub_element, reference_sub_element in zip(element, reference_element): assert_elements_equal(sub_element, reference_sub_element) def tostring(xml_string): """ Wraps `xml_string` in a div so it can be rendered, even if it has multiple roots. """ return remove_indents(f"
Some Text
.Some Text
.
mocklib.
example_function
(
a
,
b=None
,
*args
,
**kwargs
)
This is a function with a docstring.
mocklib.
example_async_function
(
)
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.