529 lines
25 KiB
Python
529 lines
25 KiB
Python
# dependencyanalyzer/gui.py
|
|
import logging
|
|
import re
|
|
import sys
|
|
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
|
|
|
|
# Updated import to include the new normalizer module
|
|
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):
|
|
"""Writes the log record to the Text widget in a thread-safe manner."""
|
|
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
|
|
|
|
self._create_widgets()
|
|
self._setup_logging_handler()
|
|
self._update_button_states()
|
|
|
|
def _create_widgets(self) -> None:
|
|
"""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.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)
|
|
|
|
# --- 2. Main Tab Control (Notebook) ---
|
|
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")
|
|
|
|
# Populate the tabs
|
|
self._create_analyzer_tab_widgets(self.analyzer_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.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."""
|
|
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."""
|
|
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)
|
|
|
|
results_frame = ttk.LabelFrame(parent_frame, text="3. Normalization Results", padding=(10, 5))
|
|
results_frame.pack(fill=tk.BOTH, expand=True, pady=5)
|
|
|
|
self.normalizer_results_tree = ttk.Treeview(
|
|
results_frame,
|
|
columns=("step", "status", "details"),
|
|
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(
|
|
results_frame, orient="vertical", command=self.normalizer_results_tree.yview
|
|
)
|
|
self.normalizer_results_tree.configure(yscrollcommand=results_scrollbar.set)
|
|
results_scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
|
|
self.normalizer_results_tree.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
|
|
|
|
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:
|
|
"""Enables/disables buttons based on the current state."""
|
|
repo_selected = bool(self.selected_repository_path)
|
|
try:
|
|
# Analyzer tab
|
|
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)
|
|
|
|
# Normalizer tab
|
|
self.normalize_button.config(state=tk.NORMAL if repo_selected else tk.DISABLED)
|
|
except tk.TclError:
|
|
pass
|
|
|
|
def _select_repository(self) -> None:
|
|
"""Opens a dialog to select a folder, then checks its status."""
|
|
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}")
|
|
|
|
# Reset state for both tabs
|
|
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())
|
|
self.normalizer_results_tree.delete(*self.normalizer_results_tree.get_children())
|
|
self._update_button_states()
|
|
|
|
status = project_normalizer.check_project_status(self.selected_repository_path)
|
|
if not all(status.values()):
|
|
message = (
|
|
"This project appears to be missing a standard setup "
|
|
"(e.g., virtual environment, pyproject.toml).\n\n"
|
|
"Would you like to switch to the 'Project Normalizer' tab to create them?"
|
|
)
|
|
if messagebox.askyesno("Project Setup Incomplete", message):
|
|
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:
|
|
error_msg = f"Error in '{task_function.__name__}': {e}"
|
|
logging.error(error_msg)
|
|
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_to_toggle = [
|
|
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_to_toggle:
|
|
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 ---
|
|
def _analyze_and_generate_reqs_threaded(self) -> None:
|
|
if not self.selected_repository_path:
|
|
messagebox.showwarning("Warning", "Please select a project repository first.")
|
|
return
|
|
|
|
self._run_long_task_threaded(
|
|
self._perform_analysis_and_generation,
|
|
callback_success=self._analysis_and_generation_callback,
|
|
)
|
|
|
|
def _perform_analysis_and_generation(
|
|
self,
|
|
) -> Tuple[Path, analyzer.DependencyInfo, analyzer.DependencyInfo]:
|
|
"""Performs analysis and generates the requirements file. Runs in a thread."""
|
|
if not self.selected_repository_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._update_button_states()
|
|
|
|
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.")
|
|
try:
|
|
with open(self.requirements_file_path, "r", encoding="utf-8") as f:
|
|
for line in f:
|
|
line = line.strip()
|
|
if line and not line.startswith("#"):
|
|
match = re.match(r"([a-zA-Z0-9._-]+)", line)
|
|
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())
|
|
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.")
|
|
else:
|
|
logging.info("No comparison results to display.")
|
|
self._update_button_states()
|
|
|
|
def _update_system_packages_threaded(self) -> None:
|
|
selected_items = self.comparison_tree.selection()
|
|
if not selected_items:
|
|
messagebox.showwarning("No Selection", "No packages selected in the table for update.")
|
|
return
|
|
|
|
packages_to_update = [
|
|
self.comparison_tree.item(item, "values")[0]
|
|
for item in selected_items
|
|
if self.comparison_tree.item(item, "values")[-1] != "OK"
|
|
]
|
|
|
|
if not packages_to_update:
|
|
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
|
|
log_level = logging.INFO if success else logging.ERROR
|
|
logging.log(log_level, f"Update Task: {message}")
|
|
if success:
|
|
logging.info("Re-running comparison after update attempt...")
|
|
self._compare_packages_threaded()
|
|
|
|
# --- 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_results_tree.delete(*self.normalizer_results_tree.get_children())
|
|
|
|
self._run_long_task_threaded(
|
|
project_normalizer.normalize_project,
|
|
self.selected_repository_path,
|
|
callback_success=self._normalize_project_callback,
|
|
)
|
|
|
|
def _normalize_project_callback(
|
|
self, results: Dict[str, Tuple[bool, str]]
|
|
) -> None:
|
|
"""Handles the results of the normalization process and updates the GUI."""
|
|
logging.info("Normalization process finished. Displaying results.")
|
|
self.normalizer_results_tree.delete(*self.normalizer_results_tree.get_children())
|
|
|
|
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",
|
|
}
|
|
|
|
# Use colors for status tags
|
|
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())
|
|
status = "Success" if success else "Failed"
|
|
|
|
self.normalizer_results_tree.insert(
|
|
"",
|
|
tk.END,
|
|
values=(step_name, status, message),
|
|
tags=(status,),
|
|
)
|
|
|
|
|
|
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() |