import os import ast from collections import defaultdict ignore_keywords = ["temp", "cache", "archive", "test", "__pycache__"] def is_standalone_function(func_name_str): # Check if the name exists in the global scope if func_name_str not in globals(): return False # Get the actual object from the string obj = globals()[func_name_str] # Verify it is a function and does NOT have a dot in its qualified name return inspect.isfunction(obj) and '.' not in obj.__qualname__ class CodebaseAnalyzer: def __init__(self, root_dir): self.root_dir = os.path.abspath(root_dir) self.defined_functions = set() # Tracks valid internal functions self.code_structure = defaultdict(list) # Maps file -> internal entities def _get_call_name(self, node): """ Extracts the callable name from an AST Call node """ if isinstance(node.func, ast.Name): return node.func.id elif isinstance(node.func, ast.Attribute): # Handles internal class method calls like self.method_name() return node.func.attr return None def first_pass_discover(self): """ First Pass: Discover all locally defined functions and methods """ for root, dirs, files in os.walk(self.root_dir): dirs[:] = [ d for d in dirs if not d.startswith(".") and not any(keyword in d for keyword in ignore_keywords) ] for file in files: if file.endswith('.py'): file_path = os.path.join(root, file) try: with open(file_path, 'r', encoding='utf-8') as f: tree = ast.parse(f.read(), filename=file) for node in ast.walk(tree): if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)): self.defined_functions.add(node.name) except Exception as e: print(f"Skipping unparseable file {file}: {e}") def second_pass_analyze(self): """ Second Pass: Map file structure and track local function calls """ for root, dirs, files in os.walk(self.root_dir): dirs[:] = [ d for d in dirs if not d.startswith(".") and not any(keyword in d for keyword in ignore_keywords) ] for file in files: if file.endswith('.py'): rel_path = os.path.relpath(os.path.join(root, file), self.root_dir) file_path = os.path.join(root, file) try: with open(file_path, 'r', encoding='utf-8') as f: tree = ast.parse(f.read(), filename=file) # Process Global/Module level functions for node in tree.body: if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)): calls = self._find_internal_calls(node) self.code_structure[rel_path].append({ 'class': 'None (Global)', 'function': node.name, 'calls': calls }) # Process Classes elif isinstance(node, ast.ClassDef): for subnode in node.body: if isinstance(subnode, (ast.FunctionDef, ast.AsyncFunctionDef)): calls = self._find_internal_calls(subnode) self.code_structure[rel_path].append({ 'class': node.name, 'function': subnode.name, 'calls': calls }) except Exception: pass def _find_internal_calls(self, func_node): """ Finds calls within a function that exist in our defined codebase """ internal_calls = set() for node in ast.walk(func_node): if isinstance(node, ast.Call): call_name = self._get_call_name(node) # Filter step: only keep it if it belongs to your codebase definitions if call_name and call_name in self.defined_functions: internal_calls.add(call_name) return list(internal_calls) def print_tree_summary(self, log_file_name): """ Prints the final summary in a clean hierarchical tree table format. """ with open(log_file_name, "w", encoding="utf-8") as log: header = f"{'File / Class / Function':<50} | {'Calls Internal Functions'}" log.write("\n") log.write("-" * len(header)) log.write("\n") log.write(header) log.write("\n") log.write("-" * len(header)) log.write("\n") for file, entities in self.code_structure.items(): log.write(f"[f] {file}") log.write("\n") # Group by class for nested visualization class_groups = defaultdict(list) for ent in entities: class_groups[ent['class']].append(ent) for class_name, items in class_groups.items(): if class_name != 'None (Global)': log.write(f"`*+--- [C] Class: {class_name}") indent = " " log.write("\n") else: indent = "" for item in items: calls_str = ", ".join(item['calls']) if item['calls'] else "None" func_line = f"{indent}`*+--- [M] {item['function']}()" # Split the string by comma and remove surrounding spaces func = [fn.strip() for fn in calls_str.split(",")] log.write(f"{func_line:<50} |") # Process the list in chunks of 'n' items m = 0 for i in range(0, len(func), 2): chunk = func[i : i + 2] # Join the chunk back with commas line_content = ", ".join(chunk) # Print with a 50-character left indent if m == 0: log.write(f" {line_content}") if m >= 1: log.write("\n") log.write(f"{' ' * 50} | {line_content}") m = m + 1 log.write("\n") log.write('\n') # --- Execution Example --- if __name__ == "__main__": # Target directory containing your project codebase # Replace '.' with your target folder path target_project_dir = "./PhotoLab" analyzer = CodebaseAnalyzer(target_project_dir) analyzer.first_pass_discover() analyzer.second_pass_analyze() analyzer.print_tree_summary("Log_Pycode.txt")