blob: 12d2eb3c6a1bbfcc5859c3b45e3d7b353f2c2291 [file] [log] [blame]
#! /usr/bin/env python
# Copyright 2016 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
from __future__ import print_function
import argparse
import imp
import os
import re
import sys
import textwrap
import types
# A markdown code block template: https://goo.gl/9EsyRi
_CODE_BLOCK_FORMAT = '''```{language}
{code}
```
'''
_DEVIL_ROOT = os.path.abspath(
os.path.join(os.path.dirname(__file__), '..', '..'))
def md_bold(raw_text):
"""Returns markdown-formatted bold text."""
return '**%s**' % md_escape(raw_text, characters='*')
def md_code(raw_text, language):
"""Returns a markdown-formatted code block in the given language."""
return _CODE_BLOCK_FORMAT.format(
language=language or '', code=md_escape(raw_text, characters='`'))
def md_escape(raw_text, characters='*_'):
"""Escapes * and _."""
def escape_char(m):
return '\\%s' % m.group(0)
pattern = '[%s]' % re.escape(characters)
return re.sub(pattern, escape_char, raw_text)
def md_heading(raw_text, level):
"""Returns markdown-formatted heading."""
adjusted_level = min(max(level, 0), 6)
return '%s%s%s' % ('#' * adjusted_level, ' ' if adjusted_level > 0 else '',
raw_text)
def md_inline_code(raw_text):
"""Returns markdown-formatted inline code."""
return '`%s`' % md_escape(raw_text, characters='`')
def md_italic(raw_text):
"""Returns markdown-formatted italic text."""
return '*%s*' % md_escape(raw_text, characters='*')
def md_link(link_text, link_target):
"""returns a markdown-formatted link."""
return '[%s](%s)' % (md_escape(link_text, characters=']'),
md_escape(link_target, characters=')'))
class MarkdownHelpFormatter(argparse.HelpFormatter):
"""A really bare-bones argparse help formatter that generates valid markdown.
This will generate something like:
usage
# **section heading**:
## **--argument-one**
```
argument-one help text
```
"""
#override
def _format_usage(self, usage, actions, groups, prefix):
usage_text = super(MarkdownHelpFormatter, self)._format_usage(
usage, actions, groups, prefix)
return md_code(usage_text, language=None)
#override
def format_help(self):
self._root_section.heading = md_heading(self._prog, level=1)
return super(MarkdownHelpFormatter, self).format_help()
#override
def start_section(self, heading):
super(MarkdownHelpFormatter, self).start_section(
md_heading(heading, level=2))
#override
def _format_action(self, action):
lines = []
action_header = self._format_action_invocation(action)
lines.append(md_heading(action_header, level=3))
if action.help:
lines.append(md_code(self._expand_help(action), language=None))
lines.extend(['', ''])
return '\n'.join(lines)
class MarkdownHelpAction(argparse.Action):
def __init__(self,
option_strings,
dest=argparse.SUPPRESS,
default=argparse.SUPPRESS,
**kwargs):
super(MarkdownHelpAction, self).__init__(
option_strings=option_strings,
dest=dest,
default=default,
nargs=0,
**kwargs)
def __call__(self, parser, namespace, values, option_string=None):
parser.formatter_class = MarkdownHelpFormatter
parser.print_help()
parser.exit()
def add_md_help_argument(parser):
"""Adds --md-help to the given argparse.ArgumentParser.
Running a script with --md-help will print the help text for that script
as valid markdown.
Args:
parser: The ArgumentParser to which --md-help should be added.
"""
parser.add_argument(
'--md-help',
action=MarkdownHelpAction,
help='print Markdown-formatted help text and exit.')
def load_module_from_path(module_path):
"""Load a module given only the path name.
Also loads package modules as necessary.
Args:
module_path: An absolute path to a python module.
Returns:
The module object for the given path.
"""
module_names = [os.path.splitext(os.path.basename(module_path))[0]]
d = os.path.dirname(module_path)
while os.path.exists(os.path.join(d, '__init__.py')):
module_names.append(os.path.basename(d))
d = os.path.dirname(d)
d = [d]
module = None
full_module_name = ''
for package_name in reversed(module_names):
if module:
d = module.__path__
full_module_name += '.'
r = imp.find_module(package_name, d)
full_module_name += package_name
module = imp.load_module(full_module_name, *r)
return module
def md_module(module_obj, module_link=None):
"""Write markdown documentation for a module.
Documents public classes and functions.
Args:
module_obj: a module object that should be documented.
Returns:
A list of markdown-formatted lines.
"""
def should_doc(name):
return (not isinstance(module_obj.__dict__[name], types.ModuleType)
and not name.startswith('_'))
stuff_to_doc = [
obj for name, obj in sorted(module_obj.__dict__.iteritems())
if should_doc(name)
]
classes_to_doc = []
functions_to_doc = []
for s in stuff_to_doc:
if isinstance(s, types.TypeType):
classes_to_doc.append(s)
elif isinstance(s, types.FunctionType):
functions_to_doc.append(s)
heading_text = module_obj.__name__
if module_link:
heading_text = md_link(heading_text, module_link)
content = [
md_heading(heading_text, level=1),
'',
md_italic('This page was autogenerated. '
'Run `devil/bin/generate_md_docs` to update'),
'',
]
for c in classes_to_doc:
content += md_class(c)
for f in functions_to_doc:
content += md_function(f)
print('\n'.join(content))
return 0
def md_class(class_obj):
"""Write markdown documentation for a class.
Documents public methods. Does not currently document subclasses.
Args:
class_obj: a types.TypeType object for the class that should be
documented.
Returns:
A list of markdown-formatted lines.
"""
content = [md_heading(md_escape(class_obj.__name__), level=2)]
content.append('')
if class_obj.__doc__:
content.extend(md_docstring(class_obj.__doc__))
def should_doc(name, obj):
return (isinstance(obj, types.FunctionType)
and (name.startswith('__') or not name.startswith('_')))
methods_to_doc = [
obj for name, obj in sorted(class_obj.__dict__.iteritems())
if should_doc(name, obj)
]
for m in methods_to_doc:
content.extend(md_function(m, class_obj=class_obj))
return content
def md_docstring(docstring):
"""Write a markdown-formatted docstring.
Returns:
A list of markdown-formatted lines.
"""
content = []
lines = textwrap.dedent(docstring).splitlines()
content.append(md_escape(lines[0]))
lines = lines[1:]
while lines and (not lines[0] or lines[0].isspace()):
lines = lines[1:]
if not all(l.isspace() for l in lines):
content.append(md_code('\n'.join(lines), language=None))
content.append('')
return content
def md_function(func_obj, class_obj=None):
"""Write markdown documentation for a function.
Args:
func_obj: a types.FunctionType object for the function that should be
documented.
Returns:
A list of markdown-formatted lines.
"""
if class_obj:
heading_text = '%s.%s' % (class_obj.__name__, func_obj.__name__)
else:
heading_text = func_obj.__name__
content = [md_heading(md_escape(heading_text), level=3)]
content.append('')
if func_obj.__doc__:
content.extend(md_docstring(func_obj.__doc__))
return content
def main(raw_args):
"""Write markdown documentation for the module at the provided path.
Args:
raw_args: the raw command-line args. Usually sys.argv[1:].
Returns:
An integer exit code. 0 for success, non-zero for failure.
"""
parser = argparse.ArgumentParser()
parser.add_argument('--module-link')
parser.add_argument('module_path', type=os.path.realpath)
args = parser.parse_args(raw_args)
return md_module(
load_module_from_path(args.module_path), module_link=args.module_link)
if __name__ == '__main__':
sys.exit(main(sys.argv[1:]))