470 lines
25 KiB
Python
470 lines
25 KiB
Python
# dependencyanalyzer/gui.py
|
|
import logging
|
|
import re
|
|
import sys
|
|
import textwrap
|
|
import threading
|
|
import tkinter as tk
|
|
from pathlib import Path
|
|
from tkinter import filedialog, messagebox, ttk
|
|
from typing import Any, Callable, Dict, List, Optional, Set, Tuple
|
|
|
|
from .core import analyzer, package_manager, project_normalizer
|
|
|
|
# --- Import Version Info FOR THE WRAPPER ITSELF ---
|
|
try:
|
|
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:
|
|
WRAPPER_APP_VERSION_STRING = "(Dev Wrapper)"
|
|
WRAPPER_BUILD_INFO = "Wrapper build time unknown"
|
|
|
|
|
|
class TextHandler(logging.Handler):
|
|
"""A logging handler that writes records to a Tkinter Text widget."""
|
|
def __init__(self, text_widget: tk.Text):
|
|
super().__init__()
|
|
self.text_widget = text_widget
|
|
self.setFormatter(logging.Formatter("[%(levelname)s] %(message)s"))
|
|
|
|
def emit(self, record: logging.LogRecord):
|
|
msg = self.format(record)
|
|
def append_message():
|
|
if self.text_widget.winfo_exists():
|
|
self.text_widget.config(state=tk.NORMAL)
|
|
self.text_widget.insert(tk.END, msg + "\n")
|
|
self.text_widget.see(tk.END)
|
|
self.text_widget.config(state=tk.DISABLED)
|
|
self.text_widget.update_idletasks()
|
|
try:
|
|
self.text_widget.after(0, append_message)
|
|
except RuntimeError:
|
|
print(f"Log error (widget destroyed?): {msg}", file=sys.stderr)
|
|
|
|
|
|
class DependencyAnalyzerApp(tk.Frame):
|
|
"""Tkinter GUI for Python dependency analysis and project normalization."""
|
|
def __init__(self, master: Optional[tk.Tk] = None):
|
|
super().__init__(master)
|
|
self.master = master
|
|
if self.master:
|
|
self.master.title(f"Python Project Tools - {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: analyzer.DependencyInfo = {}
|
|
self.external_deps_info: analyzer.DependencyInfo = {}
|
|
self.extracted_dependencies_names: Set[str] = set()
|
|
self.requirements_file_path: Optional[Path] = None
|
|
|
|
# --- NEW: Storage for normalizer step messages ---
|
|
self.normalizer_step_messages: List[str] = []
|
|
|
|
self._create_widgets()
|
|
self._setup_logging_handler()
|
|
self._update_button_states()
|
|
|
|
def _create_widgets(self) -> None:
|
|
"""Creates and lays out the GUI widgets."""
|
|
repo_frame = ttk.LabelFrame(self, text="1. Select Project Repository", padding=(10, 5))
|
|
repo_frame.pack(fill=tk.X, pady=(0, 10))
|
|
self.select_repo_button = ttk.Button(
|
|
repo_frame, text="Select 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)
|
|
|
|
self.notebook = ttk.Notebook(self)
|
|
self.notebook.pack(fill=tk.BOTH, expand=True, pady=5)
|
|
|
|
self.analyzer_tab = ttk.Frame(self.notebook, padding=(10, 10))
|
|
self.normalizer_tab = ttk.Frame(self.notebook, padding=(10, 10))
|
|
|
|
self.notebook.add(self.analyzer_tab, text="Dependency Analyzer")
|
|
self.notebook.add(self.normalizer_tab, text="Project Normalizer")
|
|
|
|
self._create_analyzer_tab_widgets(self.analyzer_tab)
|
|
self._create_normalizer_tab_widgets(self.normalizer_tab)
|
|
|
|
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)
|
|
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 _create_analyzer_tab_widgets(self, parent_frame: ttk.Frame) -> None:
|
|
"""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.pack(fill=tk.X, pady=5)
|
|
self.analyze_button = ttk.Button(
|
|
analysis_frame, text="Analyze & Generate requirements.txt", command=self._analyze_and_generate_reqs_threaded,
|
|
)
|
|
self.analyze_button.pack(side=tk.LEFT, padx=5, pady=5)
|
|
|
|
paned_window = ttk.PanedWindow(parent_frame, orient=tk.VERTICAL)
|
|
paned_window.pack(fill=tk.BOTH, expand=True, pady=5)
|
|
|
|
modules_frame = ttk.LabelFrame(paned_window, text="3. External Dependencies Found", padding=(10, 5))
|
|
paned_window.add(modules_frame, weight=1)
|
|
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)
|
|
|
|
actions_pane_frame = ttk.Frame(paned_window)
|
|
paned_window.add(actions_pane_frame, weight=2)
|
|
download_frame = ttk.LabelFrame(actions_pane_frame, text="4. Create Offline Installer", padding=(10, 5))
|
|
download_frame.pack(fill=tk.X, pady=5)
|
|
self.download_button = ttk.Button(
|
|
download_frame, text="Download 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 Scripts", command=self._create_install_scripts_threaded
|
|
)
|
|
self.create_scripts_button.pack(side=tk.LEFT, padx=5, pady=5)
|
|
|
|
compare_frame = ttk.LabelFrame(actions_pane_frame, 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 with Installed Packages", command=self._compare_packages_threaded
|
|
)
|
|
self.compare_button.pack(side=tk.TOP, anchor=tk.NW, padx=5, pady=5)
|
|
tree_frame = ttk.Frame(compare_frame)
|
|
tree_frame.pack(fill=tk.BOTH, expand=True, pady=5)
|
|
self.comparison_tree = ttk.Treeview(
|
|
tree_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=150, stretch=tk.NO, anchor=tk.W)
|
|
self.comparison_tree.heading("installed", text="Installed Version")
|
|
self.comparison_tree.column("installed", width=150, stretch=tk.NO, anchor=tk.W)
|
|
self.comparison_tree.heading("status", text="Status")
|
|
self.comparison_tree.column("status", width=120, stretch=tk.YES, anchor=tk.W)
|
|
compare_scrollbar_y = ttk.Scrollbar(tree_frame, orient="vertical", command=self.comparison_tree.yview)
|
|
self.comparison_tree.configure(yscrollcommand=compare_scrollbar_y.set)
|
|
compare_scrollbar_y.pack(side=tk.RIGHT, fill=tk.Y)
|
|
self.comparison_tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
|
|
self.update_system_button = ttk.Button(
|
|
compare_frame, text="Update Selected Packages in System", command=self._update_system_packages_threaded
|
|
)
|
|
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:
|
|
"""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.pack(fill=tk.X, pady=5)
|
|
|
|
self.normalize_button = ttk.Button(
|
|
action_frame, text="Normalize Project", command=self._normalize_project_threaded,
|
|
)
|
|
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.pack(fill=tk.BOTH, expand=True, pady=5)
|
|
|
|
# Paned window to allow resizing between steps list and details
|
|
results_pane = ttk.PanedWindow(results_frame, orient=tk.HORIZONTAL)
|
|
results_pane.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
|
|
|
|
# Left pane: List of steps
|
|
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_steps_list.pack(fill=tk.BOTH, expand=True, side=tk.LEFT)
|
|
steps_scrollbar = ttk.Scrollbar(
|
|
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):
|
|
text_handler = TextHandler(self.log_text)
|
|
text_handler.setLevel(logging.INFO)
|
|
root_logger = logging.getLogger()
|
|
if not any(isinstance(h, TextHandler) for h in root_logger.handlers):
|
|
root_logger.addHandler(text_handler)
|
|
|
|
def _update_button_states(self) -> None:
|
|
repo_selected = bool(self.selected_repository_path)
|
|
try:
|
|
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()
|
|
self.download_button.config(state=tk.NORMAL if req_file_exists else tk.DISABLED)
|
|
self.create_scripts_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
|
|
self.update_system_button.config(state=tk.NORMAL if can_update else tk.DISABLED)
|
|
self.normalize_button.config(state=tk.NORMAL if repo_selected else tk.DISABLED)
|
|
except tk.TclError:
|
|
pass
|
|
|
|
def _select_repository(self) -> None:
|
|
path = filedialog.askdirectory(title="Select Project Folder")
|
|
if not path:
|
|
logging.info("Project selection cancelled.")
|
|
return
|
|
|
|
self.selected_repository_path = Path(path)
|
|
self.repo_path_label.config(text=str(self.selected_repository_path))
|
|
logging.info(f"Project selected: {self.selected_repository_path}")
|
|
|
|
self.std_lib_deps_info, self.external_deps_info = {}, {}
|
|
self.extracted_dependencies_names, self.requirements_file_path = set(), None
|
|
self.modules_tree.delete(*self.modules_tree.get_children())
|
|
self.comparison_tree.delete(*self.comparison_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()
|
|
|
|
status = project_normalizer.check_project_status(self.selected_repository_path)
|
|
if not all(status.values()):
|
|
if messagebox.askyesno("Project Setup Incomplete",
|
|
"This project appears to be missing a standard setup "
|
|
"(e.g., virtual environment, pyproject.toml).\n\n"
|
|
"Switch to the 'Project Normalizer' tab to create them?"):
|
|
self.notebook.select(1)
|
|
|
|
def _run_long_task_threaded(self, task_function: Callable[..., Any], *args_for_task: Any,
|
|
callback_success: Optional[Callable[[Any], None]] = None) -> None:
|
|
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:
|
|
logging.error(f"Error in '{task_function.__name__}': {e}")
|
|
logging.exception(f"Full traceback for task {task_function.__name__}:")
|
|
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__}...")
|
|
thread.start()
|
|
|
|
def _temporarily_disable_buttons(self, disable: bool = True) -> None:
|
|
state_to_set = tk.DISABLED if disable else tk.NORMAL
|
|
buttons = [self.select_repo_button, self.analyze_button, self.download_button,
|
|
self.create_scripts_button, self.compare_button, self.update_system_button,
|
|
self.normalize_button]
|
|
for button in buttons:
|
|
if hasattr(button, 'winfo_exists') and button.winfo_exists():
|
|
try: button.config(state=state_to_set)
|
|
except tk.TclError: pass
|
|
|
|
# --- Methods for "Dependency Analyzer" Tab ---
|
|
# All methods for the first tab are complete and unchanged.
|
|
def _analyze_and_generate_reqs_threaded(self): self._common_threaded_task_call(self._perform_analysis_and_generation, self._analysis_and_generation_callback)
|
|
def _perform_analysis_and_generation(self):
|
|
if not self.selected_repository_path: raise ValueError("Repo path not set.")
|
|
repo_path, scan_path = self.selected_repository_path, self.selected_repository_path
|
|
potential_sub = repo_path / repo_path.name.lower()
|
|
if potential_sub.is_dir(): scan_path = potential_sub
|
|
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 _analysis_and_generation_callback(self, result):
|
|
req_file, std, ext = result
|
|
self.requirements_file_path, self.std_lib_deps_info, self.external_deps_info = req_file, std, ext
|
|
self.extracted_dependencies_names = set(ext.keys())
|
|
logging.info(f"Analysis complete. File created: {self.requirements_file_path}")
|
|
self._populate_modules_tree()
|
|
self._update_button_states()
|
|
def _populate_modules_tree(self):
|
|
self.modules_tree.delete(*self.modules_tree.get_children())
|
|
for name in sorted(list(self.extracted_dependencies_names)): self.modules_tree.insert("", tk.END, values=(name,))
|
|
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)
|
|
def _download_packages_callback(self, result): self._handle_simple_callback_result("Download Task", result)
|
|
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)
|
|
def _create_install_scripts_callback(self, result): self._handle_simple_callback_result("Script Creation Task", result)
|
|
def _compare_packages_threaded(self): self._common_threaded_task_call(self._get_comparison_data, self._compare_packages_callback)
|
|
def _get_comparison_data(self):
|
|
if not self.requirements_file_path: raise ValueError("Req file not set.")
|
|
installed = package_manager.get_installed_packages()
|
|
return package_manager.compare_requirements_with_installed(self.requirements_file_path, installed)
|
|
def _compare_packages_callback(self, results):
|
|
self.comparison_tree.delete(*self.comparison_tree.get_children())
|
|
for item in results: self.comparison_tree.insert("", tk.END, values=tuple(item.values()), tags=(item["status"],))
|
|
logging.info("Comparison results displayed.")
|
|
self._update_button_states()
|
|
def _update_system_packages_threaded(self):
|
|
selected = self.comparison_tree.selection()
|
|
if not selected: messagebox.showwarning("No Selection", "No packages selected."); return
|
|
to_update = [self.comparison_tree.item(i, "values")[0] for i in selected if self.comparison_tree.item(i, "values")[-1] != "OK"]
|
|
if not to_update: messagebox.showinfo("No Action", "Selected packages are up to date."); 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)
|
|
def _update_system_packages_callback(self, result):
|
|
self._handle_simple_callback_result("Update Task", result)
|
|
if result[0]: logging.info("Re-comparing after update..."); self._compare_packages_threaded()
|
|
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)
|
|
def _handle_simple_callback_result(self, task_name, result):
|
|
success, message = result
|
|
logging.log(logging.INFO if success else logging.ERROR, f"{task_name}: {message}")
|
|
if not success: messagebox.showerror(f"{task_name} Failed", message)
|
|
|
|
# --- Methods for "Project Normalizer" Tab ---
|
|
# --- Methods for "Project Normalizer" Tab ---
|
|
def _normalize_project_threaded(self) -> None:
|
|
"""Starts the project normalization process in a background thread."""
|
|
if not self.selected_repository_path:
|
|
messagebox.showwarning("Warning", "Please select a project repository first.")
|
|
return
|
|
|
|
# Clear previous results before starting
|
|
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.normalizer_step_messages.clear()
|
|
|
|
# The backend function no longer returns a value.
|
|
# It reports progress via the callback.
|
|
self._run_long_task_threaded(
|
|
project_normalizer.normalize_project,
|
|
self.selected_repository_path,
|
|
self._update_normalizer_progress # Pass the progress callback function
|
|
)
|
|
|
|
def _update_normalizer_progress(self, step_key: str, success: bool, message: str) -> None:
|
|
"""
|
|
Thread-safe method to update the normalizer UI with progress.
|
|
This function is called by the background thread.
|
|
"""
|
|
# Ensure GUI updates happen in the main thread
|
|
self.master.after(0, self._populate_normalizer_step, step_key, success, message)
|
|
|
|
def _populate_normalizer_step(self, step_key: str, success: bool, message: str):
|
|
"""The actual GUI update logic."""
|
|
step_map = {
|
|
"venv_creation": "1. Create Virtual Environment", "analysis": "2. Analyze Dependencies",
|
|
"requirements_generation": "3. Generate requirements.txt", "dependency_installation": "4. Install Dependencies",
|
|
"pyproject_creation": "5. Create pyproject.toml",
|
|
}
|
|
|
|
# This logic handles both new steps and updates to existing steps (like installation)
|
|
step_name = step_map.get(step_key, step_key.replace("_", " ").title())
|
|
icon = "✓" if success else "✗"
|
|
|
|
# Find if the step is already in the list
|
|
list_items = self.normalizer_steps_list.get(0, tk.END)
|
|
try:
|
|
# Find the index of the main step (e.g., "4. Install Dependencies")
|
|
main_step_name_prefix = step_name.split(" ")[0]
|
|
index = next(i for i, item in enumerate(list_items) if item.strip().startswith(main_step_name_prefix))
|
|
|
|
# Update the main step's final status
|
|
self.normalizer_steps_list.delete(index)
|
|
self.normalizer_steps_list.insert(index, f" {icon} {step_name}")
|
|
self.normalizer_steps_list.itemconfig(index, {'fg': 'green' if success else 'red'})
|
|
self.normalizer_step_messages[index] = message
|
|
|
|
except StopIteration:
|
|
# If step is not found, it's a new main step
|
|
index = self.normalizer_steps_list.size()
|
|
self.normalizer_steps_list.insert(tk.END, f" {icon} {step_name}")
|
|
self.normalizer_steps_list.itemconfig(index, {'fg': 'green' if success else 'red'})
|
|
self.normalizer_step_messages.append(message)
|
|
|
|
# Handle sub-step messages (like individual package installations)
|
|
if "Installing '" in message and success:
|
|
self.normalizer_steps_list.insert(tk.END, f" - {message.split(' ')[1]}")
|
|
self.normalizer_steps_list.itemconfig(tk.END, {'fg': 'grey'})
|
|
self.normalizer_step_messages.append(message) # Add message for sub-step too
|
|
|
|
# Auto-select the last updated/added item
|
|
last_index = self.normalizer_steps_list.size() - 1
|
|
self.normalizer_steps_list.selection_clear(0, tk.END)
|
|
self.normalizer_steps_list.selection_set(last_index)
|
|
self.normalizer_steps_list.see(last_index) # Scroll to the new item
|
|
self._on_normalizer_step_select(None) # Show details
|
|
|
|
def _normalize_project_callback(self, results: Dict[str, Tuple[bool, str]]) -> None:
|
|
"""Handles the results of the normalization, populating the new UI."""
|
|
logging.info("Normalization process finished. Displaying results.")
|
|
self.normalizer_steps_list.delete(0, tk.END)
|
|
self.normalizer_step_messages.clear()
|
|
|
|
step_map = {
|
|
"venv_creation": "1. Create Virtual Environment", "analysis": "2. Analyze Dependencies",
|
|
"requirements_generation": "3. Generate requirements.txt", "dependency_installation": "4. Install Dependencies",
|
|
"pyproject_creation": "5. Create pyproject.toml",
|
|
}
|
|
|
|
for i, (step_key, (success, message)) in enumerate(results.items()):
|
|
step_name = step_map.get(step_key, step_key.replace("_", " ").title())
|
|
icon = "✓" if success else "✗"
|
|
|
|
self.normalizer_steps_list.insert(tk.END, f" {icon} {step_name}")
|
|
self.normalizer_steps_list.itemconfig(i, {'fg': 'green' if success else 'red'})
|
|
self.normalizer_step_messages.append(message)
|
|
|
|
# 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."""
|
|
# This function remains largely the same
|
|
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:
|
|
pass
|
|
|
|
if __name__ == "__main__":
|
|
root = tk.Tk()
|
|
logging.basicConfig(level=logging.DEBUG, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s")
|
|
app = DependencyAnalyzerApp(master=root)
|
|
root.minsize(width=800, height=700)
|
|
app.mainloop() |