#Copyright ReportLab Europe Ltd. 2000-2017
#see license.txt for license details
#history https://hg.reportlab.com/hg-public/reportlab/log/tip/tools/docco/codegrab.py
#codegrab.py
"""
This grabs various Python class, method and function
headers and their doc strings to include in documents
"""

import imp
import types
import string
import os
import sys

class Struct:
    pass

def getObjectsDefinedIn(modulename, directory=None):
    """Returns two tuple of (functions, classes) defined
    in the given module.  'directory' must be the directory
    containing the script; modulename should not include
    the .py suffix"""

    if directory:
        searchpath = [directory]
    else:
        searchpath = sys.path   # searches usual Python path

    #might be a package.  If so, check the top level
    #package is there, then recalculate the path needed
    words = modulename.split('.')
    if len(words) > 1:
        packagename = words[0]
        packagefound = imp.find_module(packagename, searchpath)
        assert packagefound, "Package %s not found" % packagename
        (file, packagepath, description) = packagefound
        #now the full path should be known, if it is in the
        #package

        directory = os.path.join(*([packagepath] + words[1:-1]))
        modulename = words[-1]
        searchpath = [directory]



    #find and import the module.
    found = imp.find_module(modulename, searchpath)
    assert found, "Module %s not found" % modulename
    (file, pathname, description) = found
    mod = imp.load_module(modulename, file, pathname, description)

    #grab the code too, minus trailing newlines
    lines = open(pathname, 'r').readlines()
    lines = list(map(str.rstrip, lines))

    result = Struct()
    result.functions = []
    result.classes = []
    result.doc = mod.__doc__
    for name in dir(mod):
        value = getattr(mod, name)
        if type(value) is types.FunctionType:
            path, file = os.path.split(value.__code__.co_filename)
            root, ext = os.path.splitext(file)
            #we're possibly interested in it
            if root == modulename:
                #it was defined here
                funcObj = value
                fn = Struct()
                fn.name = name
                fn.proto = getFunctionPrototype(funcObj, lines)
                if funcObj.__doc__:
                    fn.doc = dedent(funcObj.__doc__)
                else:
                    fn.doc = '(no documentation string)'
                #is it official?
                if name[0:1] == '_':
                    fn.status = 'private'
                elif name[-1] in '0123456789':
                    fn.status = 'experimental'
                else:
                    fn.status = 'official'

                result.functions.append(fn)
        elif type(value) == type:
            if value.__module__ == modulename:
                cl = Struct()
                cl.name = name
                if value.__doc__:
                    cl.doc = dedent(value.__doc__)
                else:
                    cl.doc = "(no documentation string)"

                cl.bases = []
                for base in value.__bases__:
                    cl.bases.append(base.__name__)
                if name[0:1] == '_':
                    cl.status = 'private'
                elif name[-1] in '0123456789':
                    cl.status = 'experimental'
                else:
                    cl.status = 'official'

                cl.methods = []
                #loop over dict finding methods defined here
                # Q - should we show all methods?
                # loop over dict finding methods defined here
                items = list(value.__dict__.items())
                items.sort()
                for (key2, value2) in items:
                    if type(value2) != types.FunctionType:
                        continue # not a method
                    elif os.path.splitext(value2.__code__.co_filename)[0] == modulename:
                        continue # defined in base class
                    else:
                        #we want it
                        meth = Struct()
                        meth.name = key2
                        name2 = value2.__code__.co_name
                        meth.proto = getFunctionPrototype(value2, lines)
                        if name2!=key2:
                            meth.doc = 'pointer to '+name2
                            meth.proto = meth.proto.replace(name2,key2)
                        else:
                            if value2.__doc__:
                                meth.doc = dedent(value2.__doc__)
                            else:
                                meth.doc = "(no documentation string)"
                        #is it official?
                        if key2[0:1] == '_':
                            meth.status = 'private'
                        elif key2[-1] in '0123456789':
                            meth.status = 'experimental'
                        else:
                            meth.status = 'official'
                        cl.methods.append(meth)
                result.classes.append(cl)
    return result

def getFunctionPrototype(f, lines):
    """Pass in the function object and list of lines;
    it extracts the header as a multiline text block."""
    firstLineNo = f.__code__.co_firstlineno - 1
    lineNo = firstLineNo
    brackets = 0
    while 1:
        line = lines[lineNo]
        for char in line:
            if char == '(':
                brackets = brackets + 1
            elif char == ')':
                brackets = brackets - 1
        if brackets == 0:
            break
        else:
            lineNo = lineNo + 1

    usefulLines = lines[firstLineNo:lineNo+1]
    return '\n'.join(usefulLines)


def dedent(comment):
    """Attempts to dedent the lines to the edge. Looks at no.
    of leading spaces in line 2, and removes up to that number
    of blanks from other lines."""
    commentLines = comment.split('\n')
    if len(commentLines) < 2:
        cleaned = list(map(str.lstrip, commentLines))
    else:
        spc = 0
        for char in commentLines[1]:
            if char in string.whitespace:
                spc = spc + 1
            else:
                break
        #now check other lines
        cleaned = []
        for line in commentLines:
            for i in range(min(len(line),spc)):
                if line[0] in string.whitespace:
                    line = line[1:]
            cleaned.append(line)
    return '\n'.join(cleaned)



def dumpDoc(modulename, directory=None):
    """Test support.  Just prints docco on the module
    to standard output."""
    docco = getObjectsDefinedIn(modulename, directory)
    print('codegrab.py - ReportLab Documentation Utility')
    print('documenting', modulename + '.py')
    print('-------------------------------------------------------')
    print()
    if docco.functions == []:
        print('No functions found')
    else:
        print('Functions:')
        for f in docco.functions:
            print(f.proto)
            print('    ' + f.doc)

    if docco.classes == []:
        print('No classes found')
    else:
        print('Classes:')
        for c in docco.classes:
            print(c.name)
            print('    ' + c.doc)
            for m in c.methods:
                print(m.proto)  # it is already indented in the file!
                print('        ' + m.doc)
            print()

def test(m='reportlab.platypus.paragraph'):
    dumpDoc(m)

if __name__=='__main__':
    import sys
    print('Path to search:')
    for line in sys.path:
        print('   ',line)
    M = sys.argv[1:]
    if M==[]:
        M.append('reportlab.platypus.paragraph')
    for m in M:
        test(m)
