#!/usr/bin/env python
import inspect, sys
from difflib import unified_diff
def named_methods(class1):
result={}
for m in dir(class1):
item=eval( '.'.join([class1.__module__, class1.__name__, m]) )
if inspect.ismethod(item):
result[m]=DocBlock(item)
return result
def methods_in_common(class1,class2):
m1=named_methods(class1)
m2=named_methods(class2)
return (set(m1) & set(m2), m1, m2)
def methods_not_in_common(class1,class2):
m1=set(named_methods(class1))
m2=set(named_methods(class2))
return m1.symmetric_difference(m2)
class DocBlock(object):
"""An object to hold docstrings and code snippets for an object."""
def __init__(self, obj):
self.name=obj.__name__
self.filename=inspect.getsourcefile(obj)
codelines, lineno=inspect.getsourcelines(obj)
newcodelines=[]
ccc=iter(codelines)
for c in ccc:
while c[-2:]=='\\\n':
c=c[:-2]+ccc.next()
newcodelines.append(c.strip('\n'))
codelines=newcodelines
self.sig = codelines[0]
self.lineno = lineno
self.obj = obj
# docstring
doc = obj.__doc__
if doc is None:
self.docstring = []
self.margin = 0
return
docstring = doc.split('\n')
# find margin for docstring
if len(docstring)==1:
# one line docstring
line=docstring[0]
cline=codelines[1]
margin = len(cline) - len(cline.lstrip())
docstring[0] = ' '*margin +'"""'+ line +'"""'
else:
# multiline docstring
margin = sys.maxint
for line in docstring[1:]:
content = len(line.lstrip())
if content:
indent = len(line) - content
margin = min(margin, indent)
if margin == sys.maxint:
margin=0
self.margin=margin
docstring[0] = ' '*margin +'"""'+ docstring[0]
docstring[-1] = docstring[-1] +'"""'
if codelines[1:len(docstring)+1] != docstring:
print "Something is funny with the __doc__ attribute for %s."%self.name
print "Perhaps it was added after defined by original source code?"""
print [(u,v) for u,v in zip(codelines[1:len(docstring)+1],docstring) if u!=v]
# print "Source Code:"
# print codelines[1:len(docstring)+1]
# print "\n".join(codelines[1:len(docstring)+1])
# print
# print "__doc__: (margin=%i)"%margin
# print docstring
# print "\n".join(docstring)
raise ValueError("Docstring for %s doesn't match source code docstring."%self.name)
self.docstring=docstring
self.codelines=codelines
def get_doc_diff(self, other):
"""Return the diff of this object's docstring with another."""
docdiff=unified_diff(self.docstring, \
other.docstring,\
lineterm='', \
n=5, \
fromfile=self.filename, tofile=other.filename)
docdiff=list(docdiff)
return docdiff
def get_source_diff(self, other):
"""Return diff comparing two objects with line numbers adjusted for source files."""
docdiff = self.get_doc_diff(other)
newdiff=[]
for line in docdiff:
if line[:2]=='@@' and line[-2:]=='@@':
# pull out line numbers from diff
ol,os,nl,ns=[int(n) for ss in line[3:-3].split(' ') \
for n in ss.split(',')]
# If first line in docstring, add signature
if ol==-1 and self.sig == other.sig:
ol=self.lineno
nl=nl+other.lineno-1
os+=1
ns+=1
newline = '@@ -%i,%i +%i,%i @@'%(ol,os,nl,ns)
newdiff.append(newline)
newdiff.append(' '+self.sig)
else:
ol = abs(ol)+self.lineno
nl += other.lineno
if os==0:
ol-=1
if ns==0:
nl-=1
newline = '@@ -%i,%i +%i,%i @@'%(ol,os,nl,ns)
newdiff.append(newline)
else:
newdiff.append(line)
return newdiff
def showme(self):
print "name:",self.name
print "filename:",self.filename
print "docstring:",self.docstring
print "lineno:",self.lineno
print "object:",self.obj
def create_method_diff(method1, method2, output=True, exclude=None):
bigdiff = report_method_diff(method1, method2, False, exclude)
bigdiff=[ line for line in bigdiff if line[:5]!="*****" ]
if output:
print '\n'.join(bigdiff)
return bigdiff
def report_method_diff(method1, method2, output=True, exclude=None):
if exclude is None:
exclude=[]
assert inspect.ismethod(method1)
assert inspect.ismethod(method2)
M1=DocBlock(method1)
file1=M1.filename
M2=DocBlock(method2)
file2=M2.filename
# diff method docs
bigdiff=["************** Report of docdiff for:",\
"************** "+M1.name,\
"************** "+M2.name,\
"***************************************",\
"***** Method Document String",\
"********************************"]
bigdiff.extend( M1.get_source_diff(M2) )
if output:
print '\n'.join(bigdiff)
return bigdiff
def create_class_diff(class1, class2, output=True, exclude=None):
bigdiff = report_class_diff(class1, class2, False, exclude)
bigdiff=[ line for line in bigdiff if line[:5]!="*****" ]
if output:
print '\n'.join(bigdiff)
return bigdiff
def report_class_diff(class1, class2, output=True, exclude=None):
if exclude is None:
exclude=[]
assert inspect.isclass(class1)
assert inspect.isclass(class2)
C1=DocBlock(class1)
file1=C1.filename
C2=DocBlock(class2)
file2=C2.filename
# diff class docs
bigdiff=["************** Report of docdiff for:",\
"************** "+C1.name,\
"************** "+C2.name,\
"***************************************",\
"***** Class Document String",\
"********************************"]
bigdiff.extend( C1.get_source_diff(C2) )
# now diff methods
bigdiff.extend( ["**********************************",\
"***** Class Methods",\
"*****************************"] )
mic,mc1,mc2 = methods_in_common(class1,class2)
mic=sorted( (mc1[m].lineno,m) for m in mic )
for i,m in mic:
if m in exclude: continue
mc1_block=mc1[m]
mc2_block=mc2[m]
if mc1_block.filename == file1 and \
mc2_block.filename == file2:
newdiff = mc1_block.get_source_diff(mc2_block)
bigdiff.append("******** Method: "+m)
bigdiff.extend(newdiff[2:])
bigdiff.append("***************")
if output:
print '\n'.join(bigdiff)
return bigdiff
if __name__ == '__main__':
import sys
import networkx
usage= """Usage:
docdiff.py object1 object2 [-e|--exclude method1 [method2 ...]]
Create a report of the docstring differences between two
classes or methods. If classes, the methods of the classes
will be compared as well, excluding any methods listed after
the optional exclude switch.
To use the results as a diff file, it may be useful to
pipe through grep as:
cat output | grep -v '^\*\*\*\*\*' >diff_file
"""
nargin=len(sys.argv)
if nargin<3 or (nargin>3 and sys.argv[3] not in ['-e','--exclude']):
print usage
sys.exit()
if nargin>3:
exclude=sys.argv[4:]
else:
exclude=[]
# exclude=['neighbors','neighbors_iter',\
# 'delete_node','delete_nodes_from',\
# 'delete_edge','delete_edges_from',\
# 'out_edges','out_edges_iter',\
# ]
obj1=eval(sys.argv[1])
obj2=eval(sys.argv[2])
if inspect.isclass(obj1) and inspect.isclass(obj2):
report_class_diff(obj1,obj2,exclude=exclude)
elif inspect.ismethod(obj1) and inspect.ismethod(obj2):
report_method_diff(obj1,obj2,exclude=exclude)
else:
print
print "Objects were not recognized as classes or methods."
print "Did you forget to capitalize the class name correctly?"
print
print usage
|