1+ import  ast 
2+ import  argparse 
3+ from  pathlib  import  Path 
4+ from  collections  import  defaultdict 
5+ 
6+ # This script requires Python 3.9+ for the ast.unparse() function. 
7+ 
8+ class  ClassMethodVisitor (ast .NodeVisitor ):
9+     """ 
10+     An AST visitor that collects method names and the source code of their bodies. 
11+     """ 
12+     def  __init__ (self ):
13+         self .methods  =  defaultdict (list )
14+ 
15+     def  visit_ClassDef (self , node : ast .ClassDef ):
16+         """ 
17+         Visits a class definition, then inspects its methods. 
18+         """ 
19+         class_name  =  node .name 
20+         for  item  in  node .body :
21+             if  isinstance (item , ast .FunctionDef ):
22+                 method_name  =  item .name 
23+                 body_source  =  ast .unparse (item .body ).strip ()
24+                 self .methods [method_name ].append ((class_name , body_source ))
25+         self .generic_visit (node )
26+ 
27+ def  find_duplicate_method_content (directory : str , show_code : bool  =  True ):
28+     """ 
29+     Parses all Python files in a directory to find methods with duplicate content. 
30+ 
31+     Args: 
32+         directory: The path to the directory to inspect. 
33+         show_code: If True, prints the shared code block for each duplicate. 
34+     """ 
35+     target_dir  =  Path (directory )
36+     if  not  target_dir .is_dir ():
37+         print (f"❌ Error: '{ directory }  ' is not a valid directory." )
38+         return 
39+ 
40+     visitor  =  ClassMethodVisitor ()
41+ 
42+     for  py_file  in  target_dir .rglob ('*.py' ):
43+         try :
44+             with  open (py_file , 'r' , encoding = 'utf-8' ) as  f :
45+                 source_code  =  f .read ()
46+                 tree  =  ast .parse (source_code , filename = py_file )
47+                 visitor .visit (tree )
48+         except  Exception  as  e :
49+             print (f"⚠️ Warning: Could not process { py_file }  . Error: { e }  " )
50+ 
51+     print ("\n --- Duplicate Method Content Report ---" )
52+     duplicates_found  =  False 
53+ 
54+     for  method_name , implementations  in  sorted (visitor .methods .items ()):
55+         body_groups  =  defaultdict (list )
56+         for  class_name , body_source  in  implementations :
57+             body_groups [body_source ].append (class_name )
58+ 
59+         for  body_source , class_list  in  body_groups .items ():
60+             if  len (class_list ) >  1 :
61+                 duplicates_found  =  True 
62+                 unique_classes  =  sorted (list (set (class_list )))
63+                 print (f"\n [+] Method `def { method_name }  (...)` has identical content in { len (unique_classes )}   classes:" )
64+                 for  class_name  in  unique_classes :
65+                     print (f"  - { class_name }  " )
66+ 
67+                 # Conditionally print the shared code block based on the flag 
68+                 if  show_code :
69+                     print ("\n   Shared Code Block:" )
70+                     indented_code  =  "\n " .join ([f"    { line }  "  for  line  in  body_source .splitlines ()])
71+                     print (indented_code )
72+                     print ("  "  +  "-"  *  30 )
73+ 
74+     if  not  duplicates_found :
75+         print ("\n ✅ No methods with identical content were found across classes." )
76+ 
77+ def  main ():
78+     """Main function to set up argument parsing.""" 
79+     parser  =  argparse .ArgumentParser (
80+         description = "Find methods with identical content across Python classes in a directory." 
81+     )
82+     parser .add_argument (
83+         "directory" ,
84+         type = str ,
85+         help = "The path to the directory to inspect." 
86+     )
87+     # New argument to control output verbosity 
88+     parser .add_argument (
89+         "--hide-code" ,
90+         action = "store_true" ,
91+         help = "Do not print the shared code block for each duplicate found." 
92+     )
93+     args  =  parser .parse_args ()
94+     find_duplicate_method_content (args .directory , show_code = not  args .hide_code )
95+ 
96+ if  __name__  ==  "__main__" :
97+     main ()
0 commit comments