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