# dependencyanalyzer/gui.py import tkinter as tk from tkinter import filedialog, messagebox, ttk import threading from pathlib import Path import re # For parsing requirements.txt in fallback from typing import ( Optional, List, Dict, Set, Tuple, Any, Callable, ) # Added Any, Callable # Assuming core.py is in the same directory or package from . import core class DependencyAnalyzerApp(tk.Frame): """ Tkinter GUI application for Python dependency analysis. """ def __init__(self, master: Optional[tk.Tk] = None): super().__init__(master) self.master = master if self.master: self.master.title("Python Dependency Analyzer") self.pack(fill=tk.BOTH, expand=True, padx=10, pady=10) self.selected_repository_path: Optional[Path] = None # Store detailed dependency information self.std_lib_deps_info: core.DependencyInfo = {} self.external_deps_info: core.DependencyInfo = {} self.extracted_dependencies_names: Set[str] = ( set() ) # Just names for the Treeview self.requirements_file_path: Optional[Path] = None self._create_widgets() self._update_button_states() def _create_widgets(self) -> None: """Creates and lays out the GUI widgets.""" # --- Frame for Repository Selection --- repo_frame = ttk.LabelFrame( self, text="1. Repository Operations", padding=(10, 5) ) repo_frame.pack(fill=tk.X, pady=5) self.select_repo_button = ttk.Button( repo_frame, text="Select Repository Folder", command=self._select_repository ) self.select_repo_button.pack(side=tk.LEFT, padx=5, pady=5) self.repo_path_label = ttk.Label(repo_frame, text="No repository selected.") self.repo_path_label.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=5, pady=5) # --- Frame for Analysis and Requirements --- analysis_frame = ttk.LabelFrame( self, text="2. Analysis & Requirements", padding=(10, 5) ) analysis_frame.pack(fill=tk.X, pady=5) self.analyze_button = ttk.Button( analysis_frame, text="Analyze Project & Generate requirements.txt", command=self._analyze_and_generate_reqs_threaded, ) self.analyze_button.pack(side=tk.LEFT, padx=5, pady=5) # --- Frame for Displaying Found Modules (in requirements.txt) --- modules_frame = ttk.LabelFrame( self, text="3. External Dependencies Found", padding=(10, 5) ) modules_frame.pack(fill=tk.BOTH, expand=True, pady=5) self.modules_tree = ttk.Treeview( modules_frame, columns=("module_name",), show="headings" ) self.modules_tree.heading("module_name", text="External Module Name") self.modules_tree.column("module_name", stretch=tk.YES) modules_scrollbar = ttk.Scrollbar( modules_frame, orient="vertical", command=self.modules_tree.yview ) self.modules_tree.configure(yscrollcommand=modules_scrollbar.set) modules_scrollbar.pack(side=tk.RIGHT, fill=tk.Y) self.modules_tree.pack(fill=tk.BOTH, expand=True, padx=5, pady=5) # --- Frame for Downloading Packages and Creating Install Scripts --- download_frame = ttk.LabelFrame( self, text="4. Download Packages & Create Installers", padding=(10, 5) ) download_frame.pack(fill=tk.X, pady=5) self.download_button = ttk.Button( download_frame, text="Download Packages to _req_packages", command=self._download_packages_threaded, ) self.download_button.pack(side=tk.LEFT, padx=5, pady=5) self.create_scripts_button = ttk.Button( download_frame, text="Create install_packages.bat/sh", command=self._create_install_scripts_threaded, ) self.create_scripts_button.pack(side=tk.LEFT, padx=5, pady=5) # --- Frame for System Package Comparison --- compare_frame = ttk.LabelFrame( self, text="5. System Package Comparison", padding=(10, 5) ) compare_frame.pack(fill=tk.BOTH, expand=True, pady=5) self.compare_button = ttk.Button( compare_frame, text="Compare requirements.txt with Installed Packages", command=self._compare_packages_threaded, ) self.compare_button.pack(side=tk.TOP, anchor=tk.NW, padx=5, pady=5) self.comparison_tree = ttk.Treeview( compare_frame, columns=("package", "required", "installed", "status"), show="headings", ) self.comparison_tree.heading("package", text="Package") self.comparison_tree.heading("required", text="Required Version (from reqs)") self.comparison_tree.heading( "installed", text="Installed Version (current env)" ) self.comparison_tree.heading("status", text="Status") self.comparison_tree.column("package", width=150, stretch=tk.NO, anchor=tk.W) self.comparison_tree.column("required", width=180, stretch=tk.NO, anchor=tk.W) self.comparison_tree.column("installed", width=180, stretch=tk.NO, anchor=tk.W) self.comparison_tree.column("status", width=200, stretch=tk.YES, anchor=tk.W) compare_scrollbar_y = ttk.Scrollbar( compare_frame, orient="vertical", command=self.comparison_tree.yview ) self.comparison_tree.configure(yscrollcommand=compare_scrollbar_y.set) compare_scrollbar_x = ttk.Scrollbar( compare_frame, orient="horizontal", command=self.comparison_tree.xview ) self.comparison_tree.configure(xscrollcommand=compare_scrollbar_x.set) compare_scrollbar_y.pack(side=tk.RIGHT, fill=tk.Y) compare_scrollbar_x.pack(side=tk.BOTTOM, fill=tk.X) self.comparison_tree.pack(fill=tk.BOTH, expand=True, padx=5, pady=5) self.update_system_button = ttk.Button( compare_frame, text="Update Selected Mismatched/Not Installed Packages", command=self._update_system_packages_threaded, ) self.update_system_button.pack(side=tk.TOP, anchor=tk.NW, padx=5, pady=5) # --- Log Area --- log_frame = ttk.LabelFrame(self, text="Log Messages", padding=(10, 5)) log_frame.pack(fill=tk.X, pady=5) self.log_text = tk.Text( log_frame, height=8, state=tk.DISABLED, wrap=tk.WORD, relief=tk.SUNKEN, borderwidth=1, ) log_scrollbar = ttk.Scrollbar(log_frame, command=self.log_text.yview) self.log_text.configure(yscrollcommand=log_scrollbar.set) self.log_text.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=5, pady=5) log_scrollbar.pack(side=tk.RIGHT, fill=tk.Y) def _log_message(self, message: str, level: str = "INFO") -> None: if level == "ERROR": core.logger.error(message) elif level == "WARNING": core.logger.warning(message) else: core.logger.info(message) if self.winfo_exists(): formatted_message = f"[{level}] {message}\n" self.log_text.config(state=tk.NORMAL) self.log_text.insert(tk.END, formatted_message) self.log_text.see(tk.END) self.log_text.config(state=tk.DISABLED) if level == "ERROR": messagebox.showerror("Error", message) elif level == "WARNING": messagebox.showwarning("Warning", message) def _update_button_states(self) -> None: repo_selected = bool(self.selected_repository_path) self.analyze_button.config(state=tk.NORMAL if repo_selected else tk.DISABLED) req_file_exists_and_valid = ( self.requirements_file_path and self.requirements_file_path.exists() and bool(self.external_deps_info) ) self.download_button.config( state=tk.NORMAL if req_file_exists_and_valid else tk.DISABLED ) self.create_scripts_button.config( state=( tk.NORMAL if req_file_exists_and_valid and self.selected_repository_path else tk.DISABLED ) ) self.compare_button.config( state=tk.NORMAL if req_file_exists_and_valid else tk.DISABLED ) can_update = ( bool(self.comparison_tree.get_children()) and req_file_exists_and_valid ) self.update_system_button.config(state=tk.NORMAL if can_update else tk.DISABLED) def _select_repository(self) -> None: path = filedialog.askdirectory(title="Select Repository Folder") if path: self.selected_repository_path = Path(path) self.repo_path_label.config(text=str(self.selected_repository_path)) self._log_message(f"Repository selected: {self.selected_repository_path}") self.std_lib_deps_info = {} self.external_deps_info = {} self.extracted_dependencies_names = set() self.requirements_file_path = None self.modules_tree.delete(*self.modules_tree.get_children()) self.comparison_tree.delete(*self.comparison_tree.get_children()) else: self._log_message("Repository selection cancelled.", "INFO") self._update_button_states() # CORREZIONE APPLICATA ALLA FIRMA QUI: def _run_long_task_threaded( self, task_function: Callable[..., Any], *args_for_task: Any, # Argomenti posizionali per task_function # I seguenti sono opzionali e dovrebbero essere passati come keyword arguments callback_success: Optional[Callable[[Any], None]] = None, callback_failure: Optional[Callable[[str], None]] = None, ) -> None: """ Helper to run a function in a separate thread. `callback_success` and `callback_failure` should be specified as keyword arguments if used, to avoid ambiguity with `*args_for_task`. """ def task_wrapper(): try: result = task_function(*args_for_task) if callback_success and self.winfo_exists(): self.master.after(0, lambda: callback_success(result)) except Exception as e: error_msg = ( f"Error in threaded task '{task_function.__name__}': {str(e)}" ) self._log_message(error_msg, "ERROR") core.logger.exception( f"Full traceback for task {task_function.__name__}:" ) if callback_failure and self.winfo_exists(): self.master.after(0, lambda: callback_failure(error_msg)) finally: if self.winfo_exists(): self.master.after(0, self._temporarily_disable_buttons, False) self.master.after(10, self._update_button_states) self._temporarily_disable_buttons(True) thread = threading.Thread(target=task_wrapper, daemon=True) thread.start() self._log_message(f"Starting task: {task_function.__name__}...", "INFO") def _temporarily_disable_buttons(self, disable: bool = True) -> None: state_to_set = tk.DISABLED if disable else tk.NORMAL buttons_to_toggle = [ self.select_repo_button, self.analyze_button, self.download_button, self.create_scripts_button, self.compare_button, self.update_system_button, ] for button in buttons_to_toggle: if button.winfo_exists(): button.config(state=state_to_set) def _analyze_and_generate_reqs_threaded(self) -> None: if not self.selected_repository_path: self._log_message("Please select a repository first.", "WARNING") return self._run_long_task_threaded( self._perform_analysis_and_generation, # No *args_for_task needed for _perform_analysis_and_generation callback_success=self._analysis_and_generation_callback, # Passato come keyword ) def _perform_analysis_and_generation( self, ) -> Tuple[Path, core.DependencyInfo, core.DependencyInfo]: if not self.selected_repository_path: raise ValueError( "Repository path not selected when trying to perform analysis." ) main_script_path = core.find_main_script(self.selected_repository_path) if main_script_path: core.logger.info(f"Main script identified (for info): {main_script_path}") else: core.logger.info( "Main script (__main__.py) not found in standard locations." ) std_lib_info, external_info = core.find_project_modules_and_dependencies( self.selected_repository_path ) req_file_path = core.generate_requirements_file( self.selected_repository_path, external_info, std_lib_info ) return req_file_path, std_lib_info, external_info def _analysis_and_generation_callback( self, result: Tuple[Path, core.DependencyInfo, core.DependencyInfo] ) -> None: req_file_path, std_lib_info, external_info = result self.requirements_file_path = req_file_path self.std_lib_deps_info = std_lib_info self.external_deps_info = external_info self.extracted_dependencies_names = set(self.external_deps_info.keys()) self._log_message( f"Analysis and requirements.txt generation complete. File: {self.requirements_file_path}", "INFO", ) self._populate_modules_tree() def _populate_modules_tree(self) -> None: self.modules_tree.delete(*self.modules_tree.get_children()) if self.extracted_dependencies_names: for dep_name in sorted(list(self.extracted_dependencies_names)): self.modules_tree.insert("", tk.END, values=(dep_name,)) elif self.requirements_file_path and self.requirements_file_path.exists(): self._log_message( "Populating tree from requirements.txt as a fallback.", "DEBUG" ) try: with open(self.requirements_file_path, "r", encoding="utf-8") as f: in_external_section = False for line in f: line_content = line.strip() if "# --- External Dependencies" in line_content: in_external_section = True continue if "# --- Standard Library Modules Used" in line_content: in_external_section = False continue if ( in_external_section and line_content and not line_content.startswith("#") and not line_content.startswith("-") ): match = re.match(r"([a-zA-Z0-9._-]+)", line_content) if match: self.modules_tree.insert( "", tk.END, values=(match.group(1),) ) except Exception as e: self._log_message( f"Error reading requirements file for tree view: {e}", "WARNING" ) def _download_packages_threaded(self) -> None: if not self.requirements_file_path or not self.selected_repository_path: self._log_message( "Requirements file or repository path not set.", "WARNING" ) return if not self.external_deps_info: self._log_message( "No external dependencies found in the current analysis to download.", "INFO", ) return self._run_long_task_threaded( core.download_packages, # task_function # *args_for_task: self.selected_repository_path, self.requirements_file_path, # Keyword-only: callback_success=self._download_packages_callback, ) def _download_packages_callback(self, result: Tuple[bool, str]) -> None: success, result_message = result if success: self._log_message(result_message, "INFO") else: self._log_message(f"Package download failed: {result_message}", "ERROR") def _create_install_scripts_threaded(self) -> None: if ( not self.selected_repository_path or not self.requirements_file_path or not self.requirements_file_path.exists() ): self._log_message( "Repository not selected or requirements.txt not generated/found.", "WARNING", ) return self._run_long_task_threaded( core.create_install_scripts, # task_function # *args_for_task: self.selected_repository_path, self.requirements_file_path, # Keyword-only: callback_success=self._create_install_scripts_callback, ) def _create_install_scripts_callback(self, result: Tuple[bool, str]) -> None: success, result_message = result if success: self._log_message(result_message, "INFO") else: self._log_message(f"Script creation failed: {result_message}", "ERROR") def _compare_packages_threaded(self) -> None: if not self.requirements_file_path or not self.requirements_file_path.exists(): self._log_message( "requirements.txt not found or not generated yet for comparison.", "WARNING", ) return if ( not self.external_deps_info and self.requirements_file_path.stat().st_size == 0 ): self._log_message( "No external dependencies listed or requirements file is empty. Nothing to compare.", "INFO", ) self.comparison_tree.delete(*self.comparison_tree.get_children()) return self._run_long_task_threaded( self._get_comparison_data, # task_function # No *args_for_task for _get_comparison_data # Keyword-only: callback_success=self._compare_packages_callback, ) def _get_comparison_data(self) -> List[Dict[str, str]]: installed_packages = core.get_installed_packages() if not self.requirements_file_path: raise ValueError("Requirements file path is not set for comparison.") return core.compare_requirements_with_installed( self.requirements_file_path, installed_packages ) def _compare_packages_callback(self, results: List[Dict[str, str]]) -> None: self.comparison_tree.delete(*self.comparison_tree.get_children()) if results: for item in results: self.comparison_tree.insert( "", tk.END, values=( item["package"], item["required"], item["installed"], item["status"], ), tags=(item["status"],), ) self._log_message("Package comparison complete.", "INFO") else: self._log_message( "No packages to compare or requirements file is effectively empty for comparison.", "INFO", ) def _update_system_packages_threaded(self) -> None: selected_items_ids = self.comparison_tree.selection() if not selected_items_ids: self._log_message( "No packages selected in the comparison table for update.", "WARNING" ) if self.winfo_exists(): messagebox.showinfo( "No Selection", "Please select packages from the comparison table to update.", ) return packages_to_update: List[str] = [] for item_id in selected_items_ids: item_values = self.comparison_tree.item(item_id, "values") if not item_values: continue package_name, _, _, status = item_values # Unpack all four if ( "Mismatch" in status or "Not installed" in status or "Outdated" in status ): packages_to_update.append(str(package_name)) # Ensure it's string else: self._log_message( f"Skipping '{package_name}': status is '{status}'. No update needed.", "INFO", ) if not packages_to_update: self._log_message( "Selected packages do not require an update based on their status.", "INFO", ) if self.winfo_exists(): messagebox.showinfo( "No Action Needed", "The selected packages do not appear to need an update.", ) return if not self.selected_repository_path or not self.requirements_file_path: self._log_message( "Repository path or requirements file context not set. Cannot perform update.", "ERROR", ) return if self.winfo_exists(): confirmation = messagebox.askyesno( "Confirm Update", f"Update/install in current Python environment:\n\n{', '.join(packages_to_update)}\n\n" "Using versions from project's requirements.txt (if specified there).", ) if not confirmation: self._log_message("System package update cancelled by user.", "INFO") return self._run_long_task_threaded( core.update_system_packages, # task_function # *args_for_task: packages_to_update, self.selected_repository_path, # Keyword-only: callback_success=self._update_system_packages_callback, ) def _update_system_packages_callback(self, result: Tuple[bool, str]) -> None: success, result_message = result if success: self._log_message( f"System package update process finished. Result: {result_message}", "INFO", ) self._log_message("Re-comparing packages after update attempt...", "INFO") self._compare_packages_threaded() else: self._log_message( f"System package update failed: {result_message}", "ERROR" ) if __name__ == "__main__": import sys root = tk.Tk() app = DependencyAnalyzerApp(master=root) root.minsize(width=800, height=700) if not logging.getLogger().hasHandlers(): logging.basicConfig( stream=sys.stdout, level=logging.DEBUG, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", ) core.logger.info("Root logger configured for direct GUI test run.") else: core.logger.info("Root logger already configured. Using existing setup.") # Ensure core.logger specifically has a handler if it's not propagating to a configured root. # This can happen if core.logger.propagate is False or if root logger has no handlers. if not core.logger.handlers and ( not core.logger.parent or not core.logger.parent.handlers ): # Add a default handler to core.logger if it doesn't have one and isn't covered by root. ch = logging.StreamHandler(sys.stdout) ch.setLevel( logging.DEBUG ) # Or core.logger.level if you want to respect a level set on the logger itself formatter = logging.Formatter( "%(asctime)s - %(name)s - %(levelname)s - %(message)s" ) ch.setFormatter(formatter) core.logger.addHandler(ch) if ( core.logger.getEffectiveLevel() > logging.DEBUG ): # if level is higher than DEBUG (e.g. INFO, WARNING) core.logger.setLevel( logging.DEBUG ) # Ensure it processes DEBUG messages for testing core.logger.info( "Configured core.logger with StreamHandler for direct GUI test run because it lacked effective handlers." ) app.mainloop()