#!/usr/bin/env python # -*- coding: utf-8 -*- from __future__ import print_function from collections import namedtuple from itertools import islice import types import os import re import argparse #http://docs.python.org/2/library/argparse.html # arg name is positional is not prefixed with - or -- parser = argparse.ArgumentParser(description='Program description.') parser.add_argument('-p', '--path', metavar='PATH', type=str, required=False, default=None, help='full path relative to which paths wills be reported',action='store') parser.add_argument('-m', '--module', metavar='MODULE', type=str,required=True, help='name of package to import and examine',action='store') parser.add_argument('-G', '--github_repo', metavar='REPO', type=str,required=False, help='github project where the the coe lives, e.g. "pydata/pandas"', default=None,action='store') args = parser.parse_args() Entry=namedtuple("Entry","func path lnum undoc_names missing_args nsig_names ndoc_names") def entry_gen(root_ns,module_name): q=[root_ns] seen=set() while q: ns = q.pop() for x in dir(ns): cand = getattr(ns,x) if (isinstance(cand,types.ModuleType) and cand.__name__ not in seen and cand.__name__.startswith(module_name)): # print(cand.__name__) seen.add(cand.__name__) q.insert(0,cand) elif (isinstance(cand,(types.MethodType,types.FunctionType)) and cand not in seen and cand.__doc__): seen.add(cand) yield cand def cmp_docstring_sig(f): def build_loc(f): path=f.__code__.co_filename.split(args.path,1)[-1][1:] return dict(path=path,lnum=f.__code__.co_firstlineno) import inspect sig_names=set(inspect.getargspec(f).args) doc = f.__doc__.lower() doc = re.split("^\s*parameters\s*",doc,1,re.M)[-1] doc = re.split("^\s*returns*",doc,1,re.M)[0] doc_names={x.split(":")[0].strip() for x in doc.split("\n") if re.match("\s+[\w_]+\s*:",x)} sig_names.discard("self") doc_names.discard("kwds") doc_names.discard("kwargs") doc_names.discard("args") return Entry(func=f,path=build_loc(f)['path'],lnum=build_loc(f)['lnum'], undoc_names=sig_names.difference(doc_names), missing_args=doc_names.difference(sig_names),nsig_names=len(sig_names), ndoc_names=len(doc_names)) def format_id(i): return i def format_item_as_github_task_list( i,item,repo): tmpl = "- [ ] {id}) [{file}:{lnum} ({func_name}())]({link}) - __Missing__[{nmissing}/{total_args}]: {undoc_names}" link_tmpl = "https://github.com/{repo}/blob/master/{file}#L{lnum}" link = link_tmpl.format(repo=repo,file=item.path ,lnum=item.lnum ) s = tmpl.format(id=i,file=item.path , lnum=item.lnum, func_name=item.func.__name__, link=link, nmissing=len(item.undoc_names), total_args=item.nsig_names, undoc_names=list(item.undoc_names)) if item.missing_args: s+= " __Extra__(?): {missing_args}".format(missing_args=list(item.missing_args)) return s def format_item_as_plain(i,item): tmpl = "+{lnum} {path} {func_name}(): Missing[{nmissing}/{total_args}]={undoc_names}" s = tmpl.format(path=item.path , lnum=item.lnum, func_name=item.func.__name__, nmissing=len(item.undoc_names), total_args=item.nsig_names, undoc_names=list(item.undoc_names)) if item.missing_args: s+= " Extra(?)={missing_args}".format(missing_args=list(item.missing_args)) return s def main(): module = __import__(args.module) if not args.path: args.path=os.path.dirname(module.__file__) collect=[cmp_docstring_sig(e) for e in entry_gen(module,module.__name__)] # only include if there are missing arguments in the docstring (less false positives) # and there are at least some documented arguments collect = [e for e in collect if e.undoc_names and len(e.undoc_names) != e.nsig_names] collect.sort(key=lambda x:x.path) if args.github_repo: for i,item in enumerate(collect,1): print( format_item_as_github_task_list(i,item,args.github_repo)) else: for i,item in enumerate(collect,1): print( format_item_as_plain(i, item)) if __name__ == "__main__": import sys sys.exit(main())