#Copyright ReportLab Europe Ltd. 2000-2017 #see license.txt for license details #history https://bitbucket.org/rptlab/reportlab/history-node/tip/src/reportlab/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)