rivista la tab normalizzazione
This commit is contained in:
parent
e8485703b3
commit
6ce186efbb
@ -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()
|
||||||
|
|||||||
@ -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
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user