1#! /usr/bin/env python 2# Copyright 2016 The Chromium Authors. All rights reserved. 3# Use of this source code is governed by a BSD-style license that can be 4# found in the LICENSE file. 5 6from __future__ import print_function 7 8import argparse 9import imp 10import os 11import re 12import sys 13import textwrap 14import types 15 16# A markdown code block template: https://goo.gl/9EsyRi 17_CODE_BLOCK_FORMAT = '''```{language} 18{code} 19``` 20''' 21 22_DEVIL_ROOT = os.path.abspath( 23 os.path.join(os.path.dirname(__file__), '..', '..')) 24 25 26def md_bold(raw_text): 27 """Returns markdown-formatted bold text.""" 28 return '**%s**' % md_escape(raw_text, characters='*') 29 30 31def md_code(raw_text, language): 32 """Returns a markdown-formatted code block in the given language.""" 33 return _CODE_BLOCK_FORMAT.format( 34 language=language or '', code=md_escape(raw_text, characters='`')) 35 36 37def md_escape(raw_text, characters='*_'): 38 """Escapes * and _.""" 39 40 def escape_char(m): 41 return '\\%s' % m.group(0) 42 43 pattern = '[%s]' % re.escape(characters) 44 return re.sub(pattern, escape_char, raw_text) 45 46 47def md_heading(raw_text, level): 48 """Returns markdown-formatted heading.""" 49 adjusted_level = min(max(level, 0), 6) 50 return '%s%s%s' % ('#' * adjusted_level, ' ' if adjusted_level > 0 else '', 51 raw_text) 52 53 54def md_inline_code(raw_text): 55 """Returns markdown-formatted inline code.""" 56 return '`%s`' % md_escape(raw_text, characters='`') 57 58 59def md_italic(raw_text): 60 """Returns markdown-formatted italic text.""" 61 return '*%s*' % md_escape(raw_text, characters='*') 62 63 64def md_link(link_text, link_target): 65 """returns a markdown-formatted link.""" 66 return '[%s](%s)' % (md_escape(link_text, characters=']'), 67 md_escape(link_target, characters=')')) 68 69 70class MarkdownHelpFormatter(argparse.HelpFormatter): 71 """A really bare-bones argparse help formatter that generates valid markdown. 72 73 This will generate something like: 74 75 usage 76 77 # **section heading**: 78 79 ## **--argument-one** 80 81 ``` 82 argument-one help text 83 ``` 84 85 """ 86 87 #override 88 def _format_usage(self, usage, actions, groups, prefix): 89 usage_text = super(MarkdownHelpFormatter, self)._format_usage( 90 usage, actions, groups, prefix) 91 return md_code(usage_text, language=None) 92 93 #override 94 def format_help(self): 95 self._root_section.heading = md_heading(self._prog, level=1) 96 return super(MarkdownHelpFormatter, self).format_help() 97 98 #override 99 def start_section(self, heading): 100 super(MarkdownHelpFormatter, self).start_section( 101 md_heading(heading, level=2)) 102 103 #override 104 def _format_action(self, action): 105 lines = [] 106 action_header = self._format_action_invocation(action) 107 lines.append(md_heading(action_header, level=3)) 108 if action.help: 109 lines.append(md_code(self._expand_help(action), language=None)) 110 lines.extend(['', '']) 111 return '\n'.join(lines) 112 113 114class MarkdownHelpAction(argparse.Action): 115 def __init__(self, 116 option_strings, 117 dest=argparse.SUPPRESS, 118 default=argparse.SUPPRESS, 119 **kwargs): 120 super(MarkdownHelpAction, self).__init__( 121 option_strings=option_strings, 122 dest=dest, 123 default=default, 124 nargs=0, 125 **kwargs) 126 127 def __call__(self, parser, namespace, values, option_string=None): 128 parser.formatter_class = MarkdownHelpFormatter 129 parser.print_help() 130 parser.exit() 131 132 133def add_md_help_argument(parser): 134 """Adds --md-help to the given argparse.ArgumentParser. 135 136 Running a script with --md-help will print the help text for that script 137 as valid markdown. 138 139 Args: 140 parser: The ArgumentParser to which --md-help should be added. 141 """ 142 parser.add_argument( 143 '--md-help', 144 action=MarkdownHelpAction, 145 help='print Markdown-formatted help text and exit.') 146 147 148def load_module_from_path(module_path): 149 """Load a module given only the path name. 150 151 Also loads package modules as necessary. 152 153 Args: 154 module_path: An absolute path to a python module. 155 Returns: 156 The module object for the given path. 157 """ 158 module_names = [os.path.splitext(os.path.basename(module_path))[0]] 159 d = os.path.dirname(module_path) 160 161 while os.path.exists(os.path.join(d, '__init__.py')): 162 module_names.append(os.path.basename(d)) 163 d = os.path.dirname(d) 164 165 d = [d] 166 167 module = None 168 full_module_name = '' 169 for package_name in reversed(module_names): 170 if module: 171 d = module.__path__ 172 full_module_name += '.' 173 r = imp.find_module(package_name, d) 174 full_module_name += package_name 175 module = imp.load_module(full_module_name, *r) 176 return module 177 178 179def md_module(module_obj, module_link=None): 180 """Write markdown documentation for a module. 181 182 Documents public classes and functions. 183 184 Args: 185 module_obj: a module object that should be documented. 186 Returns: 187 A list of markdown-formatted lines. 188 """ 189 190 def should_doc(name): 191 return (not isinstance(module_obj.__dict__[name], types.ModuleType) 192 and not name.startswith('_')) 193 194 stuff_to_doc = [ 195 obj for name, obj in sorted(module_obj.__dict__.iteritems()) 196 if should_doc(name) 197 ] 198 199 classes_to_doc = [] 200 functions_to_doc = [] 201 202 for s in stuff_to_doc: 203 if isinstance(s, types.TypeType): 204 classes_to_doc.append(s) 205 elif isinstance(s, types.FunctionType): 206 functions_to_doc.append(s) 207 208 heading_text = module_obj.__name__ 209 if module_link: 210 heading_text = md_link(heading_text, module_link) 211 212 content = [ 213 md_heading(heading_text, level=1), 214 '', 215 md_italic('This page was autogenerated. ' 216 'Run `devil/bin/generate_md_docs` to update'), 217 '', 218 ] 219 220 for c in classes_to_doc: 221 content += md_class(c) 222 for f in functions_to_doc: 223 content += md_function(f) 224 225 print('\n'.join(content)) 226 227 return 0 228 229 230def md_class(class_obj): 231 """Write markdown documentation for a class. 232 233 Documents public methods. Does not currently document subclasses. 234 235 Args: 236 class_obj: a types.TypeType object for the class that should be 237 documented. 238 Returns: 239 A list of markdown-formatted lines. 240 """ 241 content = [md_heading(md_escape(class_obj.__name__), level=2)] 242 content.append('') 243 if class_obj.__doc__: 244 content.extend(md_docstring(class_obj.__doc__)) 245 246 def should_doc(name, obj): 247 return (isinstance(obj, types.FunctionType) 248 and (name.startswith('__') or not name.startswith('_'))) 249 250 methods_to_doc = [ 251 obj for name, obj in sorted(class_obj.__dict__.iteritems()) 252 if should_doc(name, obj) 253 ] 254 255 for m in methods_to_doc: 256 content.extend(md_function(m, class_obj=class_obj)) 257 258 return content 259 260 261def md_docstring(docstring): 262 """Write a markdown-formatted docstring. 263 264 Returns: 265 A list of markdown-formatted lines. 266 """ 267 content = [] 268 lines = textwrap.dedent(docstring).splitlines() 269 content.append(md_escape(lines[0])) 270 lines = lines[1:] 271 while lines and (not lines[0] or lines[0].isspace()): 272 lines = lines[1:] 273 274 if not all(l.isspace() for l in lines): 275 content.append(md_code('\n'.join(lines), language=None)) 276 content.append('') 277 return content 278 279 280def md_function(func_obj, class_obj=None): 281 """Write markdown documentation for a function. 282 283 Args: 284 func_obj: a types.FunctionType object for the function that should be 285 documented. 286 Returns: 287 A list of markdown-formatted lines. 288 """ 289 if class_obj: 290 heading_text = '%s.%s' % (class_obj.__name__, func_obj.__name__) 291 else: 292 heading_text = func_obj.__name__ 293 content = [md_heading(md_escape(heading_text), level=3)] 294 content.append('') 295 296 if func_obj.__doc__: 297 content.extend(md_docstring(func_obj.__doc__)) 298 299 return content 300 301 302def main(raw_args): 303 """Write markdown documentation for the module at the provided path. 304 305 Args: 306 raw_args: the raw command-line args. Usually sys.argv[1:]. 307 Returns: 308 An integer exit code. 0 for success, non-zero for failure. 309 """ 310 parser = argparse.ArgumentParser() 311 parser.add_argument('--module-link') 312 parser.add_argument('module_path', type=os.path.realpath) 313 args = parser.parse_args(raw_args) 314 315 return md_module( 316 load_module_from_path(args.module_path), module_link=args.module_link) 317 318 319if __name__ == '__main__': 320 sys.exit(main(sys.argv[1:])) 321