import ast import textwrap import tokenize import io from typing import Dict, List, Any, Optional from .imports import handle_import, handle_import_from from .context import enter_class, exit_class, enter_function, exit_function from .calls import handle_method_call from .widget_creation import is_widget_creation, extract_widget_info, analyze_widget_creation_commands from .extractors import extract_call_parameters, extract_parent_container from .values import extract_value, get_variable_name, analyze_lambda_complexity, extract_lambda_body, get_operator_symbol from .placements import update_widget_placement from .events import analyze_bind_event, analyze_config_command, analyze_callback, is_interactive_widget from .connections import create_widget_handler_connections class TkinterAnalyzer(ast.NodeVisitor): def __init__(self, source_code: str = ""): self.source_code = source_code self.line_offsets = [0] current = 0 if source_code: for line in source_code.splitlines(keepends=True): current += len(line) self.line_offsets.append(current) self.widgets: List[Dict[str, Any]] = [] self.window_config = {'title': 'App', 'width': 800, 'height': 600} self.imports: Dict[str, str] = {} self.variables: Dict[str, Any] = {} self.current_class: Optional[str] = None self.current_method: Optional[str] = None self.event_handlers: List[Dict[str, Any]] = [] self.command_callbacks: List[Dict[str, Any]] = [] self.bind_events: List[Dict[str, Any]] = [] self.methods: Dict[str, str] = {} def visit_Import(self, node: ast.Import): handle_import(self, node) self.generic_visit(node) def visit_ImportFrom(self, node: ast.ImportFrom): handle_import_from(self, node) self.generic_visit(node) def visit_ClassDef(self, node: ast.ClassDef): prev = self.current_class enter_class(self, node) is_tk_class = False for base in node.bases: if isinstance(base, ast.Attribute) and base.attr == 'Tk': is_tk_class = True elif isinstance(base, ast.Name) and base.id == 'Tk': is_tk_class = True if node.name == 'Application': self.window_config['className'] = node.name elif is_tk_class and self.window_config.get('className') != 'Application': self.window_config['className'] = node.name elif not self.window_config.get('className'): self.window_config['className'] = node.name self.generic_visit(node) exit_class(self, prev) def visit_FunctionDef(self, node: ast.FunctionDef): prev = self.current_method enter_function(self, node) if self.current_class and node.name not in ['__init__', 'create_widgets', 'run'] and self.source_code and node.body: try: start_line = node.lineno end_line = getattr(node, 'end_lineno', None) if end_line is None: end_line = node.body[-1].lineno lines = self.source_code.splitlines(keepends=True) func_lines = lines[start_line-1 : end_line] func_text = "".join(func_lines) body_start_idx = -1 try: tokens = list(tokenize.tokenize(io.BytesIO(func_text.encode('utf-8')).readline)) nesting = 0 colon_token = None for tok in tokens: if tok.type == tokenize.OP: if tok.string in '([{': nesting += 1 elif tok.string in ')]}': nesting -= 1 elif tok.string == ':' and nesting == 0: colon_token = tok break if colon_token: colon_line_idx = colon_token.end[0] - 1 body_lines = func_lines[colon_line_idx + 1:] colon_line = func_lines[colon_line_idx] after_colon = colon_line[colon_token.end[1]:] if after_colon.strip(): body_lines.insert(0, after_colon) body_text = "".join(body_lines) dedented_body = textwrap.dedent(body_text) sig_lines = func_lines[:colon_line_idx] sig_lines.append(colon_line[:colon_token.end[1]]) sig_raw = "".join(sig_lines).strip() if sig_raw.endswith(':'): sig_raw = sig_raw[:-1].strip() self.methods[node.name] = { 'body': dedented_body.strip(), 'signature': sig_raw } except tokenize.TokenError: pass except Exception as e: pass self.generic_visit(node) exit_function(self, prev) def visit_Assign(self, node: ast.Assign): if is_widget_creation(self, node): info = extract_widget_info(self, node) if info: self.widgets.append(info) analyze_widget_creation_commands(self, node) for target in node.targets: if isinstance(target, ast.Name): self.variables[target.id] = node.value elif isinstance(target, ast.Attribute): if isinstance(target.value, ast.Name) and target.value.id == 'self': self.variables[target.attr] = node.value self.generic_visit(node) def visit_Call(self, node: ast.Call): if isinstance(node.func, ast.Attribute): handle_method_call(self, node) self.generic_visit(node) def extract_call_parameters(self, call_node: ast.Call) -> Dict[str, Any]: return extract_call_parameters(self, call_node) def extract_value(self, node: ast.AST) -> Any: return extract_value(node) def extract_parent_container(self, call_node: ast.Call) -> str: return extract_parent_container(self, call_node) def get_variable_name(self, node: ast.AST) -> str: return get_variable_name(node) def update_widget_placement(self, target_var: str, call_node: ast.Call, method: str): return update_widget_placement(self, target_var, call_node, method) def analyze_bind_event(self, target_var: str, call_node: ast.Call): return analyze_bind_event(self, target_var, call_node) def analyze_config_command(self, target_var: str, call_node: ast.Call): return analyze_config_command(self, target_var, call_node) def analyze_callback(self, callback_node: ast.AST) -> Dict[str, Any]: return analyze_callback(self, callback_node) def analyze_lambda_complexity(self, lambda_node: ast.Lambda) -> str: return analyze_lambda_complexity(lambda_node) def extract_lambda_body(self, body_node: ast.AST) -> str: return extract_lambda_body(body_node) def get_operator_symbol(self, op_node: ast.AST) -> str: return get_operator_symbol(op_node) def analyze_widget_creation_commands(self, node: ast.Assign): return analyze_widget_creation_commands(self, node)