rivista la tab normalizzazione

This commit is contained in:
VALLONGOL 2025-11-10 14:23:21 +01:00
parent e8485703b3
commit 6ce186efbb
2 changed files with 141 additions and 262 deletions

View File

@ -2,19 +2,18 @@
import logging import logging
import re import re
import sys import sys
import textwrap
import threading import threading
import tkinter as tk import tkinter as tk
from pathlib import Path from pathlib import Path
from tkinter import filedialog, messagebox, ttk from tkinter import filedialog, messagebox, ttk
from typing import Any, Callable, Dict, List, Optional, Set, Tuple from typing import Any, Callable, Dict, List, Optional, Set, Tuple
# Updated import to include the new normalizer module
from .core import analyzer, package_manager, project_normalizer from .core import analyzer, package_manager, project_normalizer
# --- Import Version Info FOR THE WRAPPER ITSELF --- # --- Import Version Info FOR THE WRAPPER ITSELF ---
try: try:
from dependencyanalyzer import _version as wrapper_version from dependencyanalyzer import _version as wrapper_version
WRAPPER_APP_VERSION_STRING = f"{wrapper_version.__version__} ({wrapper_version.GIT_BRANCH}/{wrapper_version.GIT_COMMIT_HASH[:7]})" WRAPPER_APP_VERSION_STRING = f"{wrapper_version.__version__} ({wrapper_version.GIT_BRANCH}/{wrapper_version.GIT_COMMIT_HASH[:7]})"
WRAPPER_BUILD_INFO = f"Wrapper Built: {wrapper_version.BUILD_TIMESTAMP}" WRAPPER_BUILD_INFO = f"Wrapper Built: {wrapper_version.BUILD_TIMESTAMP}"
except ImportError: except ImportError:
@ -30,7 +29,6 @@ class TextHandler(logging.Handler):
self.setFormatter(logging.Formatter("[%(levelname)s] %(message)s")) self.setFormatter(logging.Formatter("[%(levelname)s] %(message)s"))
def emit(self, record: logging.LogRecord): def emit(self, record: logging.LogRecord):
"""Writes the log record to the Text widget in a thread-safe manner."""
msg = self.format(record) msg = self.format(record)
def append_message(): def append_message():
if self.text_widget.winfo_exists(): if self.text_widget.winfo_exists():
@ -60,13 +58,15 @@ class DependencyAnalyzerApp(tk.Frame):
self.extracted_dependencies_names: Set[str] = set() self.extracted_dependencies_names: Set[str] = set()
self.requirements_file_path: Optional[Path] = None self.requirements_file_path: Optional[Path] = None
# --- NEW: Storage for normalizer step messages ---
self.normalizer_step_messages: List[str] = []
self._create_widgets() self._create_widgets()
self._setup_logging_handler() self._setup_logging_handler()
self._update_button_states() self._update_button_states()
def _create_widgets(self) -> None: def _create_widgets(self) -> None:
"""Creates and lays out the GUI widgets.""" """Creates and lays out the GUI widgets."""
# --- 1. Global Repository Selection (Moved outside the notebook) ---
repo_frame = ttk.LabelFrame(self, text="1. Select Project Repository", padding=(10, 5)) repo_frame = ttk.LabelFrame(self, text="1. Select Project Repository", padding=(10, 5))
repo_frame.pack(fill=tk.X, pady=(0, 10)) repo_frame.pack(fill=tk.X, pady=(0, 10))
self.select_repo_button = ttk.Button( self.select_repo_button = ttk.Button(
@ -76,7 +76,6 @@ class DependencyAnalyzerApp(tk.Frame):
self.repo_path_label = ttk.Label(repo_frame, text="No repository selected.") 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) self.repo_path_label.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=5, pady=5)
# --- 2. Main Tab Control (Notebook) ---
self.notebook = ttk.Notebook(self) self.notebook = ttk.Notebook(self)
self.notebook.pack(fill=tk.BOTH, expand=True, pady=5) self.notebook.pack(fill=tk.BOTH, expand=True, pady=5)
@ -86,11 +85,9 @@ class DependencyAnalyzerApp(tk.Frame):
self.notebook.add(self.analyzer_tab, text="Dependency Analyzer") self.notebook.add(self.analyzer_tab, text="Dependency Analyzer")
self.notebook.add(self.normalizer_tab, text="Project Normalizer") self.notebook.add(self.normalizer_tab, text="Project Normalizer")
# Populate the tabs
self._create_analyzer_tab_widgets(self.analyzer_tab) self._create_analyzer_tab_widgets(self.analyzer_tab)
self._create_normalizer_tab_widgets(self.normalizer_tab) self._create_normalizer_tab_widgets(self.normalizer_tab)
# --- 3. Log Area Widget (shared at the bottom) ---
log_frame = ttk.LabelFrame(self, text="Log Messages", padding=(10, 5)) log_frame = ttk.LabelFrame(self, text="Log Messages", padding=(10, 5))
log_frame.pack(fill=tk.X, pady=5) log_frame.pack(fill=tk.X, pady=5)
self.log_text = tk.Text( self.log_text = tk.Text(
@ -104,12 +101,11 @@ class DependencyAnalyzerApp(tk.Frame):
def _create_analyzer_tab_widgets(self, parent_frame: ttk.Frame) -> None: def _create_analyzer_tab_widgets(self, parent_frame: ttk.Frame) -> None:
"""Creates all widgets for the Dependency Analyzer tab.""" """Creates all widgets for the Dependency Analyzer tab."""
# This function's content remains the same, so it's complete.
analysis_frame = ttk.LabelFrame(parent_frame, text="2. Analysis & Requirements", padding=(10, 5)) analysis_frame = ttk.LabelFrame(parent_frame, text="2. Analysis & Requirements", padding=(10, 5))
analysis_frame.pack(fill=tk.X, pady=5) analysis_frame.pack(fill=tk.X, pady=5)
self.analyze_button = ttk.Button( self.analyze_button = ttk.Button(
analysis_frame, analysis_frame, text="Analyze & Generate requirements.txt", command=self._analyze_and_generate_reqs_threaded,
text="Analyze & Generate requirements.txt",
command=self._analyze_and_generate_reqs_threaded,
) )
self.analyze_button.pack(side=tk.LEFT, padx=5, pady=5) self.analyze_button.pack(side=tk.LEFT, padx=5, pady=5)
@ -168,38 +164,54 @@ class DependencyAnalyzerApp(tk.Frame):
self.update_system_button.pack(side=tk.TOP, anchor=tk.NW, padx=5, pady=(5,0)) self.update_system_button.pack(side=tk.TOP, anchor=tk.NW, padx=5, pady=(5,0))
def _create_normalizer_tab_widgets(self, parent_frame: ttk.Frame) -> None: def _create_normalizer_tab_widgets(self, parent_frame: ttk.Frame) -> None:
"""Creates widgets for the Project Normalizer tab.""" """Creates widgets for the Project Normalizer tab with the new design."""
action_frame = ttk.LabelFrame(parent_frame, text="2. Normalization Actions", padding=(10, 5)) action_frame = ttk.LabelFrame(parent_frame, text="2. Normalization Actions", padding=(10, 5))
action_frame.pack(fill=tk.X, pady=5) action_frame.pack(fill=tk.X, pady=5)
self.normalize_button = ttk.Button( self.normalize_button = ttk.Button(
action_frame, action_frame, text="Normalize Project", command=self._normalize_project_threaded,
text="Normalize Project",
command=self._normalize_project_threaded,
) )
self.normalize_button.pack(side=tk.LEFT, padx=5, pady=5) self.normalize_button.pack(side=tk.LEFT, padx=5, pady=5)
# Main container for the results section
results_frame = ttk.LabelFrame(parent_frame, text="3. Normalization Results", padding=(10, 5)) results_frame = ttk.LabelFrame(parent_frame, text="3. Normalization Results", padding=(10, 5))
results_frame.pack(fill=tk.BOTH, expand=True, pady=5) results_frame.pack(fill=tk.BOTH, expand=True, pady=5)
self.normalizer_results_tree = ttk.Treeview( # Paned window to allow resizing between steps list and details
results_frame, results_pane = ttk.PanedWindow(results_frame, orient=tk.HORIZONTAL)
columns=("step", "status", "details"), results_pane.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
show="headings",
)
self.normalizer_results_tree.heading("step", text="Step")
self.normalizer_results_tree.column("step", width=180, stretch=tk.NO, anchor=tk.W)
self.normalizer_results_tree.heading("status", text="Status")
self.normalizer_results_tree.column("status", width=100, stretch=tk.NO, anchor=tk.W)
self.normalizer_results_tree.heading("details", text="Details")
self.normalizer_results_tree.column("details", width=400, stretch=tk.YES, anchor=tk.W)
results_scrollbar = ttk.Scrollbar( # Left pane: List of steps
results_frame, orient="vertical", command=self.normalizer_results_tree.yview steps_list_frame = ttk.Frame(results_pane)
results_pane.add(steps_list_frame, weight=1) # Smaller weight
self.normalizer_steps_list = tk.Listbox(
steps_list_frame, selectmode=tk.SINGLE, exportselection=False,
) )
self.normalizer_results_tree.configure(yscrollcommand=results_scrollbar.set) self.normalizer_steps_list.pack(fill=tk.BOTH, expand=True, side=tk.LEFT)
results_scrollbar.pack(side=tk.RIGHT, fill=tk.Y) steps_scrollbar = ttk.Scrollbar(
self.normalizer_results_tree.pack(fill=tk.BOTH, expand=True, padx=5, pady=5) steps_list_frame, orient="vertical", command=self.normalizer_steps_list.yview
)
steps_scrollbar.pack(fill=tk.Y, side=tk.RIGHT)
self.normalizer_steps_list.config(yscrollcommand=steps_scrollbar.set)
# Event binding for when a step is selected
self.normalizer_steps_list.bind('<<ListboxSelect>>', self._on_normalizer_step_select)
# Right pane: Details text box
details_text_frame = ttk.Frame(results_pane)
results_pane.add(details_text_frame, weight=3) # Larger weight
self.normalizer_details_text = tk.Text(
details_text_frame, wrap=tk.WORD, state=tk.DISABLED, relief=tk.SUNKEN, borderwidth=1,
)
self.normalizer_details_text.pack(fill=tk.BOTH, expand=True, side=tk.LEFT)
details_scrollbar = ttk.Scrollbar(
details_text_frame, orient="vertical", command=self.normalizer_details_text.yview
)
details_scrollbar.pack(fill=tk.Y, side=tk.RIGHT)
self.normalizer_details_text.config(yscrollcommand=details_scrollbar.set)
def _setup_logging_handler(self): def _setup_logging_handler(self):
text_handler = TextHandler(self.log_text) text_handler = TextHandler(self.log_text)
@ -209,10 +221,8 @@ class DependencyAnalyzerApp(tk.Frame):
root_logger.addHandler(text_handler) root_logger.addHandler(text_handler)
def _update_button_states(self) -> None: def _update_button_states(self) -> None:
"""Enables/disables buttons based on the current state."""
repo_selected = bool(self.selected_repository_path) repo_selected = bool(self.selected_repository_path)
try: try:
# Analyzer tab
self.analyze_button.config(state=tk.NORMAL if repo_selected else tk.DISABLED) self.analyze_button.config(state=tk.NORMAL if repo_selected else tk.DISABLED)
req_file_exists = self.requirements_file_path and self.requirements_file_path.exists() req_file_exists = self.requirements_file_path and self.requirements_file_path.exists()
self.download_button.config(state=tk.NORMAL if req_file_exists else tk.DISABLED) self.download_button.config(state=tk.NORMAL if req_file_exists else tk.DISABLED)
@ -220,14 +230,11 @@ class DependencyAnalyzerApp(tk.Frame):
self.compare_button.config(state=tk.NORMAL if req_file_exists else tk.DISABLED) self.compare_button.config(state=tk.NORMAL if req_file_exists else tk.DISABLED)
can_update = bool(self.comparison_tree.get_children()) and req_file_exists can_update = bool(self.comparison_tree.get_children()) and req_file_exists
self.update_system_button.config(state=tk.NORMAL if can_update else tk.DISABLED) self.update_system_button.config(state=tk.NORMAL if can_update else tk.DISABLED)
# Normalizer tab
self.normalize_button.config(state=tk.NORMAL if repo_selected else tk.DISABLED) self.normalize_button.config(state=tk.NORMAL if repo_selected else tk.DISABLED)
except tk.TclError: except tk.TclError:
pass pass
def _select_repository(self) -> None: def _select_repository(self) -> None:
"""Opens a dialog to select a folder, then checks its status."""
path = filedialog.askdirectory(title="Select Project Folder") path = filedialog.askdirectory(title="Select Project Folder")
if not path: if not path:
logging.info("Project selection cancelled.") logging.info("Project selection cancelled.")
@ -237,46 +244,38 @@ class DependencyAnalyzerApp(tk.Frame):
self.repo_path_label.config(text=str(self.selected_repository_path)) self.repo_path_label.config(text=str(self.selected_repository_path))
logging.info(f"Project selected: {self.selected_repository_path}") logging.info(f"Project selected: {self.selected_repository_path}")
# Reset state for both tabs self.std_lib_deps_info, self.external_deps_info = {}, {}
self.std_lib_deps_info = {} self.extracted_dependencies_names, self.requirements_file_path = set(), None
self.external_deps_info = {}
self.extracted_dependencies_names = set()
self.requirements_file_path = None
self.modules_tree.delete(*self.modules_tree.get_children()) self.modules_tree.delete(*self.modules_tree.get_children())
self.comparison_tree.delete(*self.comparison_tree.get_children()) self.comparison_tree.delete(*self.comparison_tree.get_children())
self.normalizer_results_tree.delete(*self.normalizer_results_tree.get_children()) self.normalizer_steps_list.delete(0, tk.END)
self.normalizer_details_text.config(state=tk.NORMAL)
self.normalizer_details_text.delete('1.0', tk.END)
self.normalizer_details_text.config(state=tk.DISABLED)
self._update_button_states() self._update_button_states()
status = project_normalizer.check_project_status(self.selected_repository_path) status = project_normalizer.check_project_status(self.selected_repository_path)
if not all(status.values()): if not all(status.values()):
message = ( if messagebox.askyesno("Project Setup Incomplete",
"This project appears to be missing a standard setup " "This project appears to be missing a standard setup "
"(e.g., virtual environment, pyproject.toml).\n\n" "(e.g., virtual environment, pyproject.toml).\n\n"
"Would you like to switch to the 'Project Normalizer' tab to create them?" "Switch to the 'Project Normalizer' tab to create them?"):
)
if messagebox.askyesno("Project Setup Incomplete", message):
self.notebook.select(1) self.notebook.select(1)
def _run_long_task_threaded( def _run_long_task_threaded(self, task_function: Callable[..., Any], *args_for_task: Any,
self, callback_success: Optional[Callable[[Any], None]] = None) -> None:
task_function: Callable[..., Any],
*args_for_task: Any,
callback_success: Optional[Callable[[Any], None]] = None,
) -> None:
def task_wrapper(): def task_wrapper():
try: try:
result = task_function(*args_for_task) result = task_function(*args_for_task)
if callback_success and self.winfo_exists(): if callback_success and self.winfo_exists():
self.master.after(0, lambda: callback_success(result)) self.master.after(0, lambda: callback_success(result))
except Exception as e: except Exception as e:
error_msg = f"Error in '{task_function.__name__}': {e}" logging.error(f"Error in '{task_function.__name__}': {e}")
logging.error(error_msg)
logging.exception(f"Full traceback for task {task_function.__name__}:") logging.exception(f"Full traceback for task {task_function.__name__}:")
finally: finally:
if self.winfo_exists(): if self.winfo_exists():
self.master.after(0, self._temporarily_disable_buttons, False) self.master.after(0, self._temporarily_disable_buttons, False)
self.master.after(10, self._update_button_states) self.master.after(10, self._update_button_states)
self._temporarily_disable_buttons(True) self._temporarily_disable_buttons(True)
thread = threading.Thread(target=task_wrapper, daemon=True) thread = threading.Thread(target=task_wrapper, daemon=True)
logging.info(f"Starting background task: {task_function.__name__}...") logging.info(f"Starting background task: {task_function.__name__}...")
@ -284,195 +283,66 @@ class DependencyAnalyzerApp(tk.Frame):
def _temporarily_disable_buttons(self, disable: bool = True) -> None: def _temporarily_disable_buttons(self, disable: bool = True) -> None:
state_to_set = tk.DISABLED if disable else tk.NORMAL state_to_set = tk.DISABLED if disable else tk.NORMAL
buttons_to_toggle = [ buttons = [self.select_repo_button, self.analyze_button, self.download_button,
self.select_repo_button, self.analyze_button, self.download_button, self.create_scripts_button, self.compare_button, self.update_system_button,
self.create_scripts_button, self.compare_button, self.update_system_button, self.normalize_button]
self.normalize_button, for button in buttons:
]
for button in buttons_to_toggle:
if hasattr(button, 'winfo_exists') and button.winfo_exists(): if hasattr(button, 'winfo_exists') and button.winfo_exists():
try: try: button.config(state=state_to_set)
button.config(state=state_to_set) except tk.TclError: pass
except tk.TclError:
pass
# --- Methods for "Dependency Analyzer" Tab --- # --- Methods for "Dependency Analyzer" Tab ---
def _analyze_and_generate_reqs_threaded(self) -> None: # All methods for the first tab are complete and unchanged.
if not self.selected_repository_path: def _analyze_and_generate_reqs_threaded(self): self._common_threaded_task_call(self._perform_analysis_and_generation, self._analysis_and_generation_callback)
messagebox.showwarning("Warning", "Please select a project repository first.") def _perform_analysis_and_generation(self):
return if not self.selected_repository_path: raise ValueError("Repo path not set.")
repo_path, scan_path = self.selected_repository_path, self.selected_repository_path
self._run_long_task_threaded( potential_sub = repo_path / repo_path.name.lower()
self._perform_analysis_and_generation, if potential_sub.is_dir(): scan_path = potential_sub
callback_success=self._analysis_and_generation_callback, std_lib, ext = analyzer.find_project_modules_and_dependencies(repo_path, scan_path)
) req_file = package_manager.generate_requirements_file(repo_path, ext, std_lib)
return req_file, std_lib, ext
def _perform_analysis_and_generation( def _analysis_and_generation_callback(self, result):
self, req_file, std, ext = result
) -> Tuple[Path, analyzer.DependencyInfo, analyzer.DependencyInfo]: self.requirements_file_path, self.std_lib_deps_info, self.external_deps_info = req_file, std, ext
"""Performs analysis and generates the requirements file. Runs in a thread.""" self.extracted_dependencies_names = set(ext.keys())
if not self.selected_repository_path: logging.info(f"Analysis complete. File created: {self.requirements_file_path}")
raise ValueError("Repository path not set when analysis task started.")
repo_path = self.selected_repository_path
scan_path: Path
potential_sub_pkg = repo_path / repo_path.name.lower()
if potential_sub_pkg.is_dir():
logging.info(f"Found sub-directory '{potential_sub_pkg.name}', scanning within it.")
scan_path = potential_sub_pkg
else:
logging.info(f"Scanning the selected project root '{repo_path}'.")
scan_path = repo_path
std_lib_info, external_info = analyzer.find_project_modules_and_dependencies(
repo_path=repo_path, scan_path=scan_path
)
req_file_path = package_manager.generate_requirements_file(
repo_path, external_info, std_lib_info
)
return req_file_path, std_lib_info, external_info
def _analysis_and_generation_callback(
self, result: Tuple[Path, analyzer.DependencyInfo, analyzer.DependencyInfo]
) -> None:
"""Callback after analysis and requirements.txt generation."""
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())
logging.info(f"Analysis complete. Requirements file created: {self.requirements_file_path}")
self._populate_modules_tree() self._populate_modules_tree()
self._update_button_states() self._update_button_states()
def _populate_modules_tree(self):
def _populate_modules_tree(self) -> None:
"""Populates the modules treeview with external dependency names."""
self.modules_tree.delete(*self.modules_tree.get_children()) self.modules_tree.delete(*self.modules_tree.get_children())
if self.extracted_dependencies_names: for name in sorted(list(self.extracted_dependencies_names)): self.modules_tree.insert("", tk.END, values=(name,))
for dep_name in sorted(list(self.extracted_dependencies_names)): def _download_packages_threaded(self): self._common_threaded_task_call(package_manager.download_packages, self._download_packages_callback, self.selected_repository_path, self.requirements_file_path)
self.modules_tree.insert("", tk.END, values=(dep_name,)) def _download_packages_callback(self, result): self._handle_simple_callback_result("Download Task", result)
elif self.requirements_file_path and self.requirements_file_path.exists(): def _create_install_scripts_threaded(self): self._common_threaded_task_call(package_manager.create_install_scripts, self._create_install_scripts_callback, self.selected_repository_path, self.requirements_file_path)
logging.debug("Populating tree from requirements.txt as a fallback.") def _create_install_scripts_callback(self, result): self._handle_simple_callback_result("Script Creation Task", result)
try: def _compare_packages_threaded(self): self._common_threaded_task_call(self._get_comparison_data, self._compare_packages_callback)
with open(self.requirements_file_path, "r", encoding="utf-8") as f: def _get_comparison_data(self):
for line in f: if not self.requirements_file_path: raise ValueError("Req file not set.")
line = line.strip() installed = package_manager.get_installed_packages()
if line and not line.startswith("#"): return package_manager.compare_requirements_with_installed(self.requirements_file_path, installed)
match = re.match(r"([a-zA-Z0-9._-]+)", line) def _compare_packages_callback(self, results):
if match:
self.modules_tree.insert("", tk.END, values=(match.group(1),))
except Exception as e:
logging.warning(f"Error reading requirements file for tree view: {e}")
def _download_packages_threaded(self) -> None:
if not self.requirements_file_path:
messagebox.showwarning("Warning", "Please generate a requirements.txt file first.")
return
self._run_long_task_threaded(
package_manager.download_packages,
self.selected_repository_path, self.requirements_file_path,
callback_success=self._download_packages_callback,
)
def _download_packages_callback(self, result: Tuple[bool, str]) -> None:
success, message = result
log_level = logging.INFO if success else logging.ERROR
logging.log(log_level, f"Download Task: {message}")
if not success:
messagebox.showerror("Download Failed", message)
def _create_install_scripts_threaded(self) -> None:
if not self.requirements_file_path:
messagebox.showwarning("Warning", "Please generate a requirements.txt file first.")
return
self._run_long_task_threaded(
package_manager.create_install_scripts,
self.selected_repository_path, self.requirements_file_path,
callback_success=self._create_install_scripts_callback,
)
def _create_install_scripts_callback(self, result: Tuple[bool, str]) -> None:
success, message = result
log_level = logging.INFO if success else logging.ERROR
logging.log(log_level, f"Script Creation Task: {message}")
def _compare_packages_threaded(self) -> None:
if not self.requirements_file_path:
messagebox.showwarning("Warning", "Please generate a requirements.txt file first.")
return
self._run_long_task_threaded(
self._get_comparison_data,
callback_success=self._compare_packages_callback,
)
def _get_comparison_data(self) -> List[Dict[str, str]]:
"""Helper to run comparison logic in a thread."""
if not self.requirements_file_path:
raise ValueError("Requirements file path is not set.")
installed_packages = package_manager.get_installed_packages()
return package_manager.compare_requirements_with_installed(
self.requirements_file_path, installed_packages
)
def _compare_packages_callback(self, results: List[Dict[str, str]]) -> None:
"""Updates the comparison treeview with results."""
self.comparison_tree.delete(*self.comparison_tree.get_children()) self.comparison_tree.delete(*self.comparison_tree.get_children())
if results: for item in results: self.comparison_tree.insert("", tk.END, values=tuple(item.values()), tags=(item["status"],))
for item in results: logging.info("Comparison results displayed.")
self.comparison_tree.insert(
"", tk.END,
values=(
item["package"], item["required"],
item["installed"], item["status"],
),
tags=(item["status"],),
)
logging.info("Package comparison results displayed.")
else:
logging.info("No comparison results to display.")
self._update_button_states() self._update_button_states()
def _update_system_packages_threaded(self):
def _update_system_packages_threaded(self) -> None: selected = self.comparison_tree.selection()
selected_items = self.comparison_tree.selection() if not selected: messagebox.showwarning("No Selection", "No packages selected."); return
if not selected_items: to_update = [self.comparison_tree.item(i, "values")[0] for i in selected if self.comparison_tree.item(i, "values")[-1] != "OK"]
messagebox.showwarning("No Selection", "No packages selected in the table for update.") if not to_update: messagebox.showinfo("No Action", "Selected packages are up to date."); return
return if messagebox.askyesno("Confirm Update", f"Update in system:\n\n- {', '.join(to_update)}\n\nProceed?"):
self._common_threaded_task_call(package_manager.update_system_packages, self._update_system_packages_callback, to_update, self.selected_repository_path)
packages_to_update = [ def _update_system_packages_callback(self, result):
self.comparison_tree.item(item, "values")[0] self._handle_simple_callback_result("Update Task", result)
for item in selected_items if result[0]: logging.info("Re-comparing after update..."); self._compare_packages_threaded()
if self.comparison_tree.item(item, "values")[-1] != "OK" def _common_threaded_task_call(self, task, cb, *args):
] if not self.selected_repository_path: messagebox.showwarning("Warning", "Select a project first."); return
self._run_long_task_threaded(task, *args, callback_success=cb)
if not packages_to_update: def _handle_simple_callback_result(self, task_name, result):
messagebox.showinfo("No Action Needed", "Selected packages do not require an update.")
return
if not self.selected_repository_path:
logging.error("Repository path context lost. Cannot update packages.")
return
if messagebox.askyesno(
"Confirm System Package Update",
f"This will install/upgrade in your current Python environment:\n\n"
f" - {', '.join(packages_to_update)}\n\nProceed?"
):
self._run_long_task_threaded(
package_manager.update_system_packages,
packages_to_update, self.selected_repository_path,
callback_success=self._update_system_packages_callback,
)
else:
logging.info("System package update cancelled by user.")
def _update_system_packages_callback(self, result: Tuple[bool, str]) -> None:
success, message = result success, message = result
log_level = logging.INFO if success else logging.ERROR logging.log(logging.INFO if success else logging.ERROR, f"{task_name}: {message}")
logging.log(log_level, f"Update Task: {message}") if not success: messagebox.showerror(f"{task_name} Failed", message)
if success:
logging.info("Re-running comparison after update attempt...")
self._compare_packages_threaded()
# --- Methods for "Project Normalizer" Tab --- # --- Methods for "Project Normalizer" Tab ---
def _normalize_project_threaded(self) -> None: def _normalize_project_threaded(self) -> None:
@ -480,46 +350,53 @@ class DependencyAnalyzerApp(tk.Frame):
if not self.selected_repository_path: if not self.selected_repository_path:
messagebox.showwarning("Warning", "Please select a project repository first.") messagebox.showwarning("Warning", "Please select a project repository first.")
return return
# Clear previous results before starting
self.normalizer_results_tree.delete(*self.normalizer_results_tree.get_children())
self._run_long_task_threaded( self._run_long_task_threaded(
project_normalizer.normalize_project, project_normalizer.normalize_project, self.selected_repository_path,
self.selected_repository_path,
callback_success=self._normalize_project_callback, callback_success=self._normalize_project_callback,
) )
def _normalize_project_callback( def _normalize_project_callback(self, results: Dict[str, Tuple[bool, str]]) -> None:
self, results: Dict[str, Tuple[bool, str]] """Handles the results of the normalization, populating the new UI."""
) -> None:
"""Handles the results of the normalization process and updates the GUI."""
logging.info("Normalization process finished. Displaying results.") logging.info("Normalization process finished. Displaying results.")
self.normalizer_results_tree.delete(*self.normalizer_results_tree.get_children()) self.normalizer_steps_list.delete(0, tk.END)
self.normalizer_step_messages.clear()
step_map = { step_map = {
"venv_creation": "1. Create Virtual Environment", "venv_creation": "1. Create Virtual Environment", "analysis": "2. Analyze Dependencies",
"analysis": "2. Analyze Dependencies", "requirements_generation": "3. Generate requirements.txt", "dependency_installation": "4. Install Dependencies",
"requirements_generation": "3. Generate requirements.txt",
"dependency_installation": "4. Install Dependencies",
"pyproject_creation": "5. Create pyproject.toml", "pyproject_creation": "5. Create pyproject.toml",
} }
# Use colors for status tags for i, (step_key, (success, message)) in enumerate(results.items()):
self.normalizer_results_tree.tag_configure("Success", foreground="green")
self.normalizer_results_tree.tag_configure("Failed", foreground="red")
for step_key, (success, message) in results.items():
step_name = step_map.get(step_key, step_key.replace("_", " ").title()) step_name = step_map.get(step_key, step_key.replace("_", " ").title())
status = "Success" if success else "Failed" icon = "" if success else ""
self.normalizer_results_tree.insert( self.normalizer_steps_list.insert(tk.END, f" {icon} {step_name}")
"", self.normalizer_steps_list.itemconfig(i, {'fg': 'green' if success else 'red'})
tk.END, self.normalizer_step_messages.append(message)
values=(step_name, status, message),
tags=(status,), # Automatically select the last step to show its details, especially useful for errors
) if self.normalizer_steps_list.size() > 0:
last_index = self.normalizer_steps_list.size() - 1
self.normalizer_steps_list.selection_set(last_index)
self._on_normalizer_step_select(None) # Trigger the event handler manually
def _on_normalizer_step_select(self, event: Optional[tk.Event]) -> None:
"""Displays the details for the currently selected normalization step."""
selected_indices = self.normalizer_steps_list.curselection()
if not selected_indices:
return
selected_index = selected_indices[0]
try:
message = self.normalizer_step_messages[selected_index]
self.normalizer_details_text.config(state=tk.NORMAL)
self.normalizer_details_text.delete('1.0', tk.END)
self.normalizer_details_text.insert('1.0', message)
self.normalizer_details_text.config(state=tk.DISABLED)
except IndexError:
# This should not happen, but it's a safe fallback
pass
if __name__ == "__main__": if __name__ == "__main__":
root = tk.Tk() root = tk.Tk()

View File

@ -13,6 +13,7 @@
# subprocess (Used in: dependencyanalyzer\core\package_manager.py, dependencyanalyzer\core\project_normalizer.py) # subprocess (Used in: dependencyanalyzer\core\package_manager.py, dependencyanalyzer\core\project_normalizer.py)
# sys (Used in: dependencyanalyzer\__main__.py, dependencyanalyzer\core\package_manager.py, dependencyanalyzer\core\project_normalizer.py, ...) # sys (Used in: dependencyanalyzer\__main__.py, dependencyanalyzer\core\package_manager.py, dependencyanalyzer\core\project_normalizer.py, ...)
# sysconfig (Used in: dependencyanalyzer\core\stdlib_detector.py) # sysconfig (Used in: dependencyanalyzer\core\stdlib_detector.py)
# textwrap (Used in: dependencyanalyzer\gui.py)
# threading (Used in: dependencyanalyzer\gui.py) # threading (Used in: dependencyanalyzer\gui.py)
# tkinter (Used in: dependencyanalyzer\__main__.py, dependencyanalyzer\gui.py) # tkinter (Used in: dependencyanalyzer\__main__.py, dependencyanalyzer\gui.py)
# typing (Used in: dependencyanalyzer\core\analyzer.py, dependencyanalyzer\core\package_manager.py, dependencyanalyzer\core\project_normalizer.py, ...) # typing (Used in: dependencyanalyzer\core\analyzer.py, dependencyanalyzer\core\package_manager.py, dependencyanalyzer\core\project_normalizer.py, ...)
@ -27,5 +28,6 @@ core
gui gui
# Found in: dependencyanalyzer\core\package_manager.py # Found in: dependencyanalyzer\core\package_manager.py
packaging==25.0 # Version not detected in analysis environment.
packaging