Blame external/pybind11/tools/mkdoc.py

Packit 534379
#!/usr/bin/env python3
Packit 534379
#
Packit 534379
#  Syntax: mkdoc.py [-I<path> ..] [.. a list of header files ..]
Packit 534379
#
Packit 534379
#  Extract documentation from C++ header files to use it in Python bindings
Packit 534379
#
Packit 534379
Packit 534379
import os
Packit 534379
import sys
Packit 534379
import platform
Packit 534379
import re
Packit 534379
import textwrap
Packit 534379
Packit 534379
from clang import cindex
Packit 534379
from clang.cindex import CursorKind
Packit 534379
from collections import OrderedDict
Packit 534379
from glob import glob
Packit 534379
from threading import Thread, Semaphore
Packit 534379
from multiprocessing import cpu_count
Packit 534379
Packit 534379
RECURSE_LIST = [
Packit 534379
    CursorKind.TRANSLATION_UNIT,
Packit 534379
    CursorKind.NAMESPACE,
Packit 534379
    CursorKind.CLASS_DECL,
Packit 534379
    CursorKind.STRUCT_DECL,
Packit 534379
    CursorKind.ENUM_DECL,
Packit 534379
    CursorKind.CLASS_TEMPLATE
Packit 534379
]
Packit 534379
Packit 534379
PRINT_LIST = [
Packit 534379
    CursorKind.CLASS_DECL,
Packit 534379
    CursorKind.STRUCT_DECL,
Packit 534379
    CursorKind.ENUM_DECL,
Packit 534379
    CursorKind.ENUM_CONSTANT_DECL,
Packit 534379
    CursorKind.CLASS_TEMPLATE,
Packit 534379
    CursorKind.FUNCTION_DECL,
Packit 534379
    CursorKind.FUNCTION_TEMPLATE,
Packit 534379
    CursorKind.CONVERSION_FUNCTION,
Packit 534379
    CursorKind.CXX_METHOD,
Packit 534379
    CursorKind.CONSTRUCTOR,
Packit 534379
    CursorKind.FIELD_DECL
Packit 534379
]
Packit 534379
Packit 534379
PREFIX_BLACKLIST = [
Packit 534379
    CursorKind.TRANSLATION_UNIT
Packit 534379
]
Packit 534379
Packit 534379
CPP_OPERATORS = {
Packit 534379
    '<=': 'le', '>=': 'ge', '==': 'eq', '!=': 'ne', '[]': 'array',
Packit 534379
    '+=': 'iadd', '-=': 'isub', '*=': 'imul', '/=': 'idiv', '%=':
Packit 534379
    'imod', '&=': 'iand', '|=': 'ior', '^=': 'ixor', '<<=': 'ilshift',
Packit 534379
    '>>=': 'irshift', '++': 'inc', '--': 'dec', '<<': 'lshift', '>>':
Packit 534379
    'rshift', '&&': 'land', '||': 'lor', '!': 'lnot', '~': 'bnot',
Packit 534379
    '&': 'band', '|': 'bor', '+': 'add', '-': 'sub', '*': 'mul', '/':
Packit 534379
    'div', '%': 'mod', '<': 'lt', '>': 'gt', '=': 'assign', '()': 'call'
Packit 534379
}
Packit 534379
Packit 534379
CPP_OPERATORS = OrderedDict(
Packit 534379
    sorted(CPP_OPERATORS.items(), key=lambda t: -len(t[0])))
Packit 534379
Packit 534379
job_count = cpu_count()
Packit 534379
job_semaphore = Semaphore(job_count)
Packit 534379
Packit 534379
Packit 534379
class NoFilenamesError(ValueError):
Packit 534379
    pass
Packit 534379
Packit 534379
Packit 534379
def d(s):
Packit 534379
    return s if isinstance(s, str) else s.decode('utf8')
Packit 534379
Packit 534379
Packit 534379
def sanitize_name(name):
Packit 534379
    name = re.sub(r'type-parameter-0-([0-9]+)', r'T\1', name)
Packit 534379
    for k, v in CPP_OPERATORS.items():
Packit 534379
        name = name.replace('operator%s' % k, 'operator_%s' % v)
Packit 534379
    name = re.sub('<.*>', '', name)
Packit 534379
    name = ''.join([ch if ch.isalnum() else '_' for ch in name])
Packit 534379
    name = re.sub('_$', '', re.sub('_+', '_', name))
Packit 534379
    return '__doc_' + name
Packit 534379
Packit 534379
Packit 534379
def process_comment(comment):
Packit 534379
    result = ''
Packit 534379
Packit 534379
    # Remove C++ comment syntax
Packit 534379
    leading_spaces = float('inf')
Packit 534379
    for s in comment.expandtabs(tabsize=4).splitlines():
Packit 534379
        s = s.strip()
Packit 534379
        if s.startswith('/*'):
Packit 534379
            s = s[2:].lstrip('*')
Packit 534379
        elif s.endswith('*/'):
Packit 534379
            s = s[:-2].rstrip('*')
Packit 534379
        elif s.startswith('///'):
Packit 534379
            s = s[3:]
Packit 534379
        if s.startswith('*'):
Packit 534379
            s = s[1:]
Packit 534379
        if len(s) > 0:
Packit 534379
            leading_spaces = min(leading_spaces, len(s) - len(s.lstrip()))
Packit 534379
        result += s + '\n'
Packit 534379
Packit 534379
    if leading_spaces != float('inf'):
Packit 534379
        result2 = ""
Packit 534379
        for s in result.splitlines():
Packit 534379
            result2 += s[leading_spaces:] + '\n'
Packit 534379
        result = result2
Packit 534379
Packit 534379
    # Doxygen tags
Packit 534379
    cpp_group = '([\w:]+)'
Packit 534379
    param_group = '([\[\w:\]]+)'
Packit 534379
Packit 534379
    s = result
Packit 534379
    s = re.sub(r'\\c\s+%s' % cpp_group, r'``\1``', s)
Packit 534379
    s = re.sub(r'\\a\s+%s' % cpp_group, r'*\1*', s)
Packit 534379
    s = re.sub(r'\\e\s+%s' % cpp_group, r'*\1*', s)
Packit 534379
    s = re.sub(r'\\em\s+%s' % cpp_group, r'*\1*', s)
Packit 534379
    s = re.sub(r'\\b\s+%s' % cpp_group, r'**\1**', s)
Packit 534379
    s = re.sub(r'\\ingroup\s+%s' % cpp_group, r'', s)
Packit 534379
    s = re.sub(r'\\param%s?\s+%s' % (param_group, cpp_group),
Packit 534379
               r'\n\n$Parameter ``\2``:\n\n', s)
Packit 534379
    s = re.sub(r'\\tparam%s?\s+%s' % (param_group, cpp_group),
Packit 534379
               r'\n\n$Template parameter ``\2``:\n\n', s)
Packit 534379
Packit 534379
    for in_, out_ in {
Packit 534379
        'return': 'Returns',
Packit 534379
        'author': 'Author',
Packit 534379
        'authors': 'Authors',
Packit 534379
        'copyright': 'Copyright',
Packit 534379
        'date': 'Date',
Packit 534379
        'remark': 'Remark',
Packit 534379
        'sa': 'See also',
Packit 534379
        'see': 'See also',
Packit 534379
        'extends': 'Extends',
Packit 534379
        'throw': 'Throws',
Packit 534379
        'throws': 'Throws'
Packit 534379
    }.items():
Packit 534379
        s = re.sub(r'\\%s\s*' % in_, r'\n\n$%s:\n\n' % out_, s)
Packit 534379
Packit 534379
    s = re.sub(r'\\details\s*', r'\n\n', s)
Packit 534379
    s = re.sub(r'\\brief\s*', r'', s)
Packit 534379
    s = re.sub(r'\\short\s*', r'', s)
Packit 534379
    s = re.sub(r'\\ref\s*', r'', s)
Packit 534379
Packit 534379
    s = re.sub(r'\\code\s?(.*?)\s?\\endcode',
Packit 534379
               r"```\n\1\n```\n", s, flags=re.DOTALL)
Packit 534379
Packit 534379
    # HTML/TeX tags
Packit 534379
    s = re.sub(r'<tt>(.*?)</tt>', r'``\1``', s, flags=re.DOTALL)
Packit 534379
    s = re.sub(r'
(.*?)
', r"```\n\1\n```\n", s, flags=re.DOTALL)
Packit 534379
    s = re.sub(r'(.*?)', r'*\1*', s, flags=re.DOTALL)
Packit 534379
    s = re.sub(r'(.*?)', r'**\1**', s, flags=re.DOTALL)
Packit 534379
    s = re.sub(r'\\f\$(.*?)\\f\$', r'$\1$', s, flags=re.DOTALL)
Packit 534379
    s = re.sub(r'
  • ', r'\n\n* ', s)
  • Packit 534379
        s = re.sub(r'</?ul>', r'', s)
    Packit 534379
        s = re.sub(r'', r'\n\n', s)
    Packit 534379
    Packit 534379
        s = s.replace('``true``', '``True``')
    Packit 534379
        s = s.replace('``false``', '``False``')
    Packit 534379
    Packit 534379
        # Re-flow text
    Packit 534379
        wrapper = textwrap.TextWrapper()
    Packit 534379
        wrapper.expand_tabs = True
    Packit 534379
        wrapper.replace_whitespace = True
    Packit 534379
        wrapper.drop_whitespace = True
    Packit 534379
        wrapper.width = 70
    Packit 534379
        wrapper.initial_indent = wrapper.subsequent_indent = ''
    Packit 534379
    Packit 534379
        result = ''
    Packit 534379
        in_code_segment = False
    Packit 534379
        for x in re.split(r'(```)', s):
    Packit 534379
            if x == '```':
    Packit 534379
                if not in_code_segment:
    Packit 534379
                    result += '```\n'
    Packit 534379
                else:
    Packit 534379
                    result += '\n```\n\n'
    Packit 534379
                in_code_segment = not in_code_segment
    Packit 534379
            elif in_code_segment:
    Packit 534379
                result += x.strip()
    Packit 534379
            else:
    Packit 534379
                for y in re.split(r'(?: *\n *){2,}', x):
    Packit 534379
                    wrapped = wrapper.fill(re.sub(r'\s+', ' ', y).strip())
    Packit 534379
                    if len(wrapped) > 0 and wrapped[0] == '$':
    Packit 534379
                        result += wrapped[1:] + '\n'
    Packit 534379
                        wrapper.initial_indent = \
    Packit 534379
                            wrapper.subsequent_indent = ' ' * 4
    Packit 534379
                    else:
    Packit 534379
                        if len(wrapped) > 0:
    Packit 534379
                            result += wrapped + '\n\n'
    Packit 534379
                        wrapper.initial_indent = wrapper.subsequent_indent = ''
    Packit 534379
        return result.rstrip().lstrip('\n')
    Packit 534379
    Packit 534379
    Packit 534379
    def extract(filename, node, prefix, output):
    Packit 534379
        if not (node.location.file is None or
    Packit 534379
                os.path.samefile(d(node.location.file.name), filename)):
    Packit 534379
            return 0
    Packit 534379
        if node.kind in RECURSE_LIST:
    Packit 534379
            sub_prefix = prefix
    Packit 534379
            if node.kind not in PREFIX_BLACKLIST:
    Packit 534379
                if len(sub_prefix) > 0:
    Packit 534379
                    sub_prefix += '_'
    Packit 534379
                sub_prefix += d(node.spelling)
    Packit 534379
            for i in node.get_children():
    Packit 534379
                extract(filename, i, sub_prefix, output)
    Packit 534379
        if node.kind in PRINT_LIST:
    Packit 534379
            comment = d(node.raw_comment) if node.raw_comment is not None else ''
    Packit 534379
            comment = process_comment(comment)
    Packit 534379
            sub_prefix = prefix
    Packit 534379
            if len(sub_prefix) > 0:
    Packit 534379
                sub_prefix += '_'
    Packit 534379
            if len(node.spelling) > 0:
    Packit 534379
                name = sanitize_name(sub_prefix + d(node.spelling))
    Packit 534379
                output.append((name, filename, comment))
    Packit 534379
    Packit 534379
    Packit 534379
    class ExtractionThread(Thread):
    Packit 534379
        def __init__(self, filename, parameters, output):
    Packit 534379
            Thread.__init__(self)
    Packit 534379
            self.filename = filename
    Packit 534379
            self.parameters = parameters
    Packit 534379
            self.output = output
    Packit 534379
            job_semaphore.acquire()
    Packit 534379
    Packit 534379
        def run(self):
    Packit 534379
            print('Processing "%s" ..' % self.filename, file=sys.stderr)
    Packit 534379
            try:
    Packit 534379
                index = cindex.Index(
    Packit 534379
                    cindex.conf.lib.clang_createIndex(False, True))
    Packit 534379
                tu = index.parse(self.filename, self.parameters)
    Packit 534379
                extract(self.filename, tu.cursor, '', self.output)
    Packit 534379
            finally:
    Packit 534379
                job_semaphore.release()
    Packit 534379
    Packit 534379
    Packit 534379
    def read_args(args):
    Packit 534379
        parameters = []
    Packit 534379
        filenames = []
    Packit 534379
        if "-x" not in args:
    Packit 534379
            parameters.extend(['-x', 'c++'])
    Packit 534379
        if not any(it.startswith("-std=") for it in args):
    Packit 534379
            parameters.append('-std=c++11')
    Packit 534379
    Packit 534379
        if platform.system() == 'Darwin':
    Packit 534379
            dev_path = '/Applications/Xcode.app/Contents/Developer/'
    Packit 534379
            lib_dir = dev_path + 'Toolchains/XcodeDefault.xctoolchain/usr/lib/'
    Packit 534379
            sdk_dir = dev_path + 'Platforms/MacOSX.platform/Developer/SDKs'
    Packit 534379
            libclang = lib_dir + 'libclang.dylib'
    Packit 534379
    Packit 534379
            if os.path.exists(libclang):
    Packit 534379
                cindex.Config.set_library_path(os.path.dirname(libclang))
    Packit 534379
    Packit 534379
            if os.path.exists(sdk_dir):
    Packit 534379
                sysroot_dir = os.path.join(sdk_dir, next(os.walk(sdk_dir))[1][0])
    Packit 534379
                parameters.append('-isysroot')
    Packit 534379
                parameters.append(sysroot_dir)
    Packit 534379
        elif platform.system() == 'Linux':
    Packit 534379
            # clang doesn't find its own base includes by default on Linux,
    Packit 534379
            # but different distros install them in different paths.
    Packit 534379
            # Try to autodetect, preferring the highest numbered version.
    Packit 534379
            def clang_folder_version(d):
    Packit 534379
                return [int(ver) for ver in re.findall(r'(?
    Packit 534379
            clang_include_dir = max((
    Packit 534379
                path
    Packit 534379
                for libdir in ['lib64', 'lib', 'lib32']
    Packit 534379
                for path in glob('/usr/%s/clang/*/include' % libdir)
    Packit 534379
                if os.path.isdir(path)
    Packit 534379
            ), default=None, key=clang_folder_version)
    Packit 534379
            if clang_include_dir:
    Packit 534379
                parameters.extend(['-isystem', clang_include_dir])
    Packit 534379
    Packit 534379
        for item in args:
    Packit 534379
            if item.startswith('-'):
    Packit 534379
                parameters.append(item)
    Packit 534379
            else:
    Packit 534379
                filenames.append(item)
    Packit 534379
    Packit 534379
        if len(filenames) == 0:
    Packit 534379
            raise NoFilenamesError("args parameter did not contain any filenames")
    Packit 534379
    Packit 534379
        return parameters, filenames
    Packit 534379
    Packit 534379
    Packit 534379
    def extract_all(args):
    Packit 534379
        parameters, filenames = read_args(args)
    Packit 534379
        output = []
    Packit 534379
        for filename in filenames:
    Packit 534379
            thr = ExtractionThread(filename, parameters, output)
    Packit 534379
            thr.start()
    Packit 534379
    Packit 534379
        print('Waiting for jobs to finish ..', file=sys.stderr)
    Packit 534379
        for i in range(job_count):
    Packit 534379
            job_semaphore.acquire()
    Packit 534379
    Packit 534379
        return output
    Packit 534379
    Packit 534379
    Packit 534379
    def write_header(comments, out_file=sys.stdout):
    Packit 534379
        print('''/*
    Packit 534379
      This file contains docstrings for the Python bindings.
    Packit 534379
      Do not edit! These were automatically extracted by mkdoc.py
    Packit 534379
     */
    Packit 534379
    Packit 534379
    #define __EXPAND(x)                                      x
    Packit 534379
    #define __COUNT(_1, _2, _3, _4, _5, _6, _7, COUNT, ...)  COUNT
    Packit 534379
    #define __VA_SIZE(...)                                   __EXPAND(__COUNT(__VA_ARGS__, 7, 6, 5, 4, 3, 2, 1))
    Packit 534379
    #define __CAT1(a, b)                                     a ## b
    Packit 534379
    #define __CAT2(a, b)                                     __CAT1(a, b)
    Packit 534379
    #define __DOC1(n1)                                       __doc_##n1
    Packit 534379
    #define __DOC2(n1, n2)                                   __doc_##n1##_##n2
    Packit 534379
    #define __DOC3(n1, n2, n3)                               __doc_##n1##_##n2##_##n3
    Packit 534379
    #define __DOC4(n1, n2, n3, n4)                           __doc_##n1##_##n2##_##n3##_##n4
    Packit 534379
    #define __DOC5(n1, n2, n3, n4, n5)                       __doc_##n1##_##n2##_##n3##_##n4##_##n5
    Packit 534379
    #define __DOC6(n1, n2, n3, n4, n5, n6)                   __doc_##n1##_##n2##_##n3##_##n4##_##n5##_##n6
    Packit 534379
    #define __DOC7(n1, n2, n3, n4, n5, n6, n7)               __doc_##n1##_##n2##_##n3##_##n4##_##n5##_##n6##_##n7
    Packit 534379
    #define DOC(...)                                         __EXPAND(__EXPAND(__CAT2(__DOC, __VA_SIZE(__VA_ARGS__)))(__VA_ARGS__))
    Packit 534379
    Packit 534379
    #if defined(__GNUG__)
    Packit 534379
    #pragma GCC diagnostic push
    Packit 534379
    #pragma GCC diagnostic ignored "-Wunused-variable"
    Packit 534379
    #endif
    Packit 534379
    ''', file=out_file)
    Packit 534379
    Packit 534379
    Packit 534379
        name_ctr = 1
    Packit 534379
        name_prev = None
    Packit 534379
        for name, _, comment in list(sorted(comments, key=lambda x: (x[0], x[1]))):
    Packit 534379
            if name == name_prev:
    Packit 534379
                name_ctr += 1
    Packit 534379
                name = name + "_%i" % name_ctr
    Packit 534379
            else:
    Packit 534379
                name_prev = name
    Packit 534379
                name_ctr = 1
    Packit 534379
            print('\nstatic const char *%s =%sR"doc(%s)doc";' %
    Packit 534379
                  (name, '\n' if '\n' in comment else ' ', comment), file=out_file)
    Packit 534379
    Packit 534379
        print('''
    Packit 534379
    #if defined(__GNUG__)
    Packit 534379
    #pragma GCC diagnostic pop
    Packit 534379
    #endif
    Packit 534379
    ''', file=out_file)
    Packit 534379
    Packit 534379
    Packit 534379
    def mkdoc(args):
    Packit 534379
        args = list(args)
    Packit 534379
        out_path = None
    Packit 534379
        for idx, arg in enumerate(args):
    Packit 534379
            if arg.startswith("-o"):
    Packit 534379
                args.remove(arg)
    Packit 534379
                try:
    Packit 534379
                    out_path = arg[2:] or args.pop(idx)
    Packit 534379
                except IndexError:
    Packit 534379
                    print("-o flag requires an argument")
    Packit 534379
                    exit(-1)
    Packit 534379
                break
    Packit 534379
    Packit 534379
        comments = extract_all(args)
    Packit 534379
    Packit 534379
        if out_path:
    Packit 534379
            try:
    Packit 534379
                with open(out_path, 'w') as out_file:
    Packit 534379
                    write_header(comments, out_file)
    Packit 534379
            except:
    Packit 534379
                # In the event of an error, don't leave a partially-written
    Packit 534379
                # output file.
    Packit 534379
                try:
    Packit 534379
                    os.unlink(out_path)
    Packit 534379
                except:
    Packit 534379
                    pass
    Packit 534379
                raise
    Packit 534379
        else:
    Packit 534379
            write_header(comments)
    Packit 534379
    Packit 534379
    Packit 534379
    if __name__ == '__main__':
    Packit 534379
        try:
    Packit 534379
            mkdoc(sys.argv[1:])
    Packit 534379
        except NoFilenamesError:
    Packit 534379
            print('Syntax: %s [.. a list of header files ..]' % sys.argv[0])
    Packit 534379
            exit(-1)