491 lines
26 KiB
Python
491 lines
26 KiB
Python
# 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
|
|
import logging # Import standard logging
|
|
import queue # For potential use in handler, though root.after is often sufficient
|
|
from typing import Optional, List, Dict, Set, Tuple, Any, Callable
|
|
|
|
# Assuming core.py is in the same directory or package
|
|
from . import core # Imports core, core.logger should be available via this
|
|
|
|
# --- Custom Logging Handler ---
|
|
class TextHandler(logging.Handler):
|
|
"""
|
|
A logging handler that writes records to a Tkinter Text widget.
|
|
Uses root.after(0, ...) to ensure thread safety for GUI updates.
|
|
"""
|
|
def __init__(self, text_widget: tk.Text):
|
|
super().__init__()
|
|
self.text_widget = text_widget
|
|
# Basic formatter, can be customized
|
|
self.setFormatter(logging.Formatter('[%(levelname)s] %(message)s'))
|
|
|
|
def emit(self, record: logging.LogRecord):
|
|
"""
|
|
Writes the log record to the Text widget in a thread-safe manner.
|
|
"""
|
|
msg = self.format(record)
|
|
|
|
def append_message():
|
|
"""Internal function to append message in the GUI thread."""
|
|
if self.text_widget.winfo_exists(): # Check if widget still exists
|
|
self.text_widget.config(state=tk.NORMAL)
|
|
self.text_widget.insert(tk.END, msg + '\n')
|
|
self.text_widget.see(tk.END) # Scroll to the end
|
|
self.text_widget.config(state=tk.DISABLED)
|
|
self.text_widget.update_idletasks() # Process pending events
|
|
|
|
try:
|
|
self.text_widget.after(0, append_message)
|
|
except Exception as e:
|
|
# Fallback or logging if `after` fails (e.g., window destroyed during shutdown)
|
|
print(f"Error updating log widget: {e}", file=sys.stderr)
|
|
|
|
|
|
# --- Import Version Info FOR THE WRAPPER ITSELF ---
|
|
try:
|
|
# Use absolute import based on package name
|
|
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_BUILD_INFO = f"Wrapper Built: {wrapper_version.BUILD_TIMESTAMP}"
|
|
except ImportError:
|
|
# This might happen if you run the wrapper directly from source
|
|
# without generating its _version.py first (if you use that approach for the wrapper itself)
|
|
WRAPPER_APP_VERSION_STRING = "(Dev Wrapper)"
|
|
WRAPPER_BUILD_INFO = "Wrapper build time unknown"
|
|
# --- End Import Version Info ---
|
|
|
|
# --- Constants for Version Generation ---
|
|
DEFAULT_VERSION = "0.0.0+unknown"
|
|
DEFAULT_COMMIT = "Unknown"
|
|
DEFAULT_BRANCH = "Unknown"
|
|
# --- End Constants ---
|
|
|
|
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(f"Python Dependency Analyzer - {WRAPPER_APP_VERSION_STRING}")
|
|
self.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
|
|
|
|
self.selected_repository_path: Optional[Path] = None
|
|
self.std_lib_deps_info: core.DependencyInfo = {}
|
|
self.external_deps_info: core.DependencyInfo = {}
|
|
self.extracted_dependencies_names: Set[str] = set()
|
|
self.requirements_file_path: Optional[Path] = None
|
|
|
|
self._create_widgets()
|
|
self._setup_logging_handler() # Setup the custom handler
|
|
self._update_button_states()
|
|
|
|
def _create_widgets(self) -> None:
|
|
"""Creates and lays out the GUI widgets."""
|
|
# --- 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)
|
|
|
|
# --- Analysis ---
|
|
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)
|
|
|
|
# --- External Dependencies TreeView ---
|
|
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)
|
|
|
|
# --- Download & 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)
|
|
|
|
# --- Comparison Frame ---
|
|
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.column("package", width=150, stretch=tk.NO, anchor=tk.W)
|
|
self.comparison_tree.heading("required", text="Required Version"); self.comparison_tree.column("required", width=180, stretch=tk.NO, anchor=tk.W)
|
|
self.comparison_tree.heading("installed", text="Installed Version"); self.comparison_tree.column("installed", width=180, stretch=tk.NO, anchor=tk.W)
|
|
self.comparison_tree.heading("status", text="Status"); 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 Widget ---
|
|
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=10, state=tk.DISABLED, wrap=tk.WORD, relief=tk.SUNKEN, borderwidth=1) # Increased height
|
|
log_scrollbar = ttk.Scrollbar(log_frame, command=self.log_text.yview); self.log_text.configure(yscrollcommand=log_scrollbar.set)
|
|
log_scrollbar.pack(side=tk.RIGHT, fill=tk.Y); self.log_text.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=5, pady=5)
|
|
|
|
|
|
def _setup_logging_handler(self):
|
|
"""Creates and adds the custom TextHandler to the root logger."""
|
|
text_handler = TextHandler(self.log_text)
|
|
text_handler.setLevel(logging.INFO) # Handler level - messages below this won't pass
|
|
|
|
root_logger = logging.getLogger()
|
|
# Ensure root logger's level is low enough (set in __main__.py or here)
|
|
if root_logger.level > logging.INFO: # If root logger is set to WARNING or higher
|
|
root_logger.setLevel(logging.INFO) # Lower it to allow INFO messages through
|
|
print(f"Root logger level adjusted to INFO by GUI setup.")
|
|
|
|
if not any(isinstance(h, TextHandler) for h in root_logger.handlers):
|
|
root_logger.addHandler(text_handler)
|
|
print("TextHandler added to root logger.")
|
|
else:
|
|
print("TextHandler already present in root logger.")
|
|
|
|
|
|
def _log_message(self, message: str, level: str = "INFO") -> None:
|
|
"""
|
|
Logs a message using the root logger and shows popups for Warning/Error.
|
|
The TextHandler attached to the root logger will display the message in the GUI.
|
|
"""
|
|
log_level = getattr(logging, level.upper(), logging.INFO)
|
|
|
|
# Log to the root logger, TextHandler will pick it up
|
|
logging.log(log_level, message)
|
|
|
|
# Show popups in the GUI thread for important user feedback
|
|
if self.winfo_exists():
|
|
if level == "ERROR":
|
|
messagebox.showerror("Error", message)
|
|
elif level == "WARNING":
|
|
messagebox.showwarning("Warning", message)
|
|
|
|
|
|
def _update_button_states(self) -> None:
|
|
"""Enables/disables buttons based on the current state."""
|
|
repo_selected = bool(self.selected_repository_path)
|
|
try:
|
|
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 repo_selected 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)
|
|
except tk.TclError: pass # Ignore if widgets destroyed
|
|
|
|
|
|
def _select_repository(self) -> None:
|
|
"""Opens a dialog to select the repository folder."""
|
|
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))
|
|
logging.info(f"Repository selected: {self.selected_repository_path}") # Log via root
|
|
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:
|
|
logging.info("Repository selection cancelled.") # Log via root
|
|
self._update_button_states()
|
|
|
|
# CORREZIONE APPLICATA ALLA FIRMA QUI (rimosso *)
|
|
def _run_long_task_threaded(
|
|
self,
|
|
task_function: Callable[..., Any],
|
|
*args_for_task: Any, # Argomenti posizionali per task_function
|
|
# I seguenti sono opzionali e DEVONO 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` MUST be specified as keyword arguments.
|
|
"""
|
|
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)}"
|
|
# Log error using the standard logger; TextHandler will display it.
|
|
logging.error(error_msg) # Log simple message to GUI
|
|
logging.exception(f"Full traceback for task {task_function.__name__}:") # Log traceback to console/file
|
|
if callback_failure and self.winfo_exists():
|
|
self.master.after(0, lambda: callback_failure(error_msg)) # Pass simple message
|
|
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)
|
|
logging.info(f"Starting background task: {task_function.__name__}...") # Log via root
|
|
thread.start()
|
|
|
|
|
|
def _temporarily_disable_buttons(self, disable: bool = True) -> None:
|
|
"""Disables or enables key operational buttons."""
|
|
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 hasattr(button, 'winfo_exists') and button.winfo_exists():
|
|
try: button.config(state=state_to_set)
|
|
except tk.TclError: pass # Ignore if widget destroyed
|
|
|
|
|
|
# --- Analysis and Requirements Generation ---
|
|
def _analyze_and_generate_reqs_threaded(self) -> None:
|
|
"""Starts the background thread for analysis and requirements generation."""
|
|
if not self.selected_repository_path:
|
|
self._log_message("Please select a repository first.", "WARNING") # User feedback
|
|
return
|
|
# Start the background task
|
|
self._run_long_task_threaded(
|
|
self._perform_analysis_and_generation, # Function to run in thread
|
|
# No specific arguments needed here for the task function itself
|
|
callback_success=self._analysis_and_generation_callback # Keyword arg
|
|
)
|
|
|
|
def _perform_analysis_and_generation(self) -> Tuple[Path, core.DependencyInfo, core.DependencyInfo]:
|
|
"""
|
|
Determines scan path, performs analysis via core function, generates file.
|
|
This method runs in the background thread.
|
|
Uses self.selected_repository_path set by the GUI.
|
|
"""
|
|
if not self.selected_repository_path:
|
|
# This should ideally not be reached if called correctly, but added safeguard
|
|
raise ValueError("Repository path is not selected when analysis task started.")
|
|
|
|
repo_path = self.selected_repository_path
|
|
repo_name_lower = repo_path.name.lower()
|
|
potential_sub_package_path = repo_path / repo_name_lower
|
|
|
|
scan_path: Path # Define the path to start scanning from
|
|
|
|
# Check if a sub-directory with the lowercased repo name exists
|
|
if potential_sub_package_path.is_dir():
|
|
logging.info(f"Found sub-directory '{repo_name_lower}', scanning within it.")
|
|
scan_path = potential_sub_package_path
|
|
else:
|
|
logging.info(f"Sub-directory '{repo_name_lower}' not found, scanning the selected repository root '{repo_path}'.")
|
|
scan_path = repo_path
|
|
|
|
# Optional: Find main script (less critical now but kept for potential info)
|
|
# This logic might also need adjustment based on scan_path vs repo_path if needed elsewhere
|
|
core.find_main_script(repo_path) # Still checks relative to repo_path
|
|
|
|
# Call the core analysis function with both repo_path and scan_path
|
|
std_lib_info, external_info = core.find_project_modules_and_dependencies(
|
|
repo_path=repo_path, # Pass original root for relative paths
|
|
scan_path=scan_path # Pass the determined path to actually scan
|
|
)
|
|
|
|
# Generate requirements file (uses repo_path for the output file location)
|
|
req_file_path = core.generate_requirements_file(
|
|
repo_path, # Output in the root selected by user
|
|
external_info,
|
|
std_lib_info
|
|
)
|
|
|
|
# Return all necessary results for the callback
|
|
return req_file_path, std_lib_info, external_info
|
|
|
|
def _analysis_and_generation_callback(self, result: Tuple[Path, core.DependencyInfo, core.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 and requirements generation complete. File: {self.requirements_file_path}") # Log via root
|
|
self._populate_modules_tree()
|
|
|
|
def _populate_modules_tree(self) -> None:
|
|
"""Populates the modules treeview with EXTERNAL dependency names."""
|
|
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():
|
|
logging.debug("Populating tree from requirements.txt as a fallback.") # Log via root
|
|
try:
|
|
with open(self.requirements_file_path, 'r', encoding='utf-8') as f:
|
|
# ... (fallback logic remains the same) ...
|
|
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") # User feedback
|
|
|
|
|
|
# --- Download Packages ---
|
|
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,
|
|
# *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:
|
|
"""Handles the result of the download operation."""
|
|
success, result_message = result
|
|
log_level = logging.ERROR if not success else logging.INFO
|
|
logging.log(log_level, f"Download Task Summary: {result_message}") # Log via root
|
|
|
|
|
|
# --- Create Install Scripts ---
|
|
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,
|
|
# *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:
|
|
"""Handles the result of the script creation."""
|
|
success, result_message = result
|
|
log_level = logging.ERROR if not success else logging.INFO
|
|
logging.log(log_level, f"Script Creation Task Summary: {result_message}") # Log via root
|
|
|
|
|
|
# --- Compare Packages ---
|
|
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 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 detected and requirements file empty.", "INFO")
|
|
self.comparison_tree.delete(*self.comparison_tree.get_children()); return
|
|
|
|
self._run_long_task_threaded(
|
|
self._get_comparison_data,
|
|
# No *args_for_task
|
|
callback_success=self._compare_packages_callback # keyword-only
|
|
)
|
|
|
|
def _get_comparison_data(self) -> List[Dict[str,str]]:
|
|
"""Helper to run comparison logic in thread."""
|
|
if not self.requirements_file_path: raise ValueError("Requirements file path is not set.")
|
|
# core functions log internally
|
|
installed_packages = core.get_installed_packages()
|
|
return core.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."""
|
|
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"],))
|
|
logging.info("Package comparison results displayed.") # Log via root
|
|
else:
|
|
logging.info("No comparison results to display.") # Log via root
|
|
|
|
|
|
# --- Update System Packages ---
|
|
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"); 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
|
|
if "Mismatch" in status or "Not installed" in status or "Outdated" in status:
|
|
packages_to_update.append(str(package_name))
|
|
else:
|
|
logging.info(f"Skipping '{package_name}': status '{status}'.") # Log via root
|
|
|
|
if not packages_to_update:
|
|
self._log_message("Selected packages do not require an update.", "INFO"); return
|
|
|
|
if not self.selected_repository_path or not self.requirements_file_path:
|
|
self._log_message("Repository path or requirements file context not set.", "ERROR"); return
|
|
|
|
if self.winfo_exists(): # Show confirmation dialog
|
|
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).")
|
|
if not confirmation:
|
|
logging.info("System package update cancelled by user."); return # Log via root
|
|
|
|
self._run_long_task_threaded(
|
|
core.update_system_packages,
|
|
# *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:
|
|
"""Handles result of system package update."""
|
|
success, result_message = result
|
|
log_level = logging.ERROR if not success else logging.INFO
|
|
logging.log(log_level, f"Update Task Summary: {result_message}") # Log via root
|
|
if success:
|
|
logging.info("Re-comparing packages after update attempt...") # Log via root
|
|
self._compare_packages_threaded()
|
|
|
|
|
|
if __name__ == '__main__':
|
|
# This block is only for direct testing of gui.py
|
|
import sys
|
|
root = tk.Tk()
|
|
|
|
# --- Setup logging for direct testing ---
|
|
log_level = logging.DEBUG # Use DEBUG for testing
|
|
log_format = '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
|
kwargs = {'force': True} if sys.version_info >= (3, 8) else {}
|
|
logging.basicConfig(level=log_level, format=log_format, stream=sys.stdout, **kwargs) # type: ignore
|
|
logging.info("Root logger configured for direct GUI test run.")
|
|
# --- End Logging Setup ---
|
|
|
|
app = DependencyAnalyzerApp(master=root)
|
|
root.minsize(width=800, height=700)
|
|
app.mainloop() |