rivista la tab normalizzazione

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

View File

@ -2,19 +2,18 @@
import logging
import 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
# 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:
@ -30,7 +29,6 @@ class TextHandler(logging.Handler):
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():
@ -60,13 +58,15 @@ class DependencyAnalyzerApp(tk.Frame):
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."""
# --- 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(
@ -76,7 +76,6 @@ class DependencyAnalyzerApp(tk.Frame):
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)
@ -86,11 +85,9 @@ class DependencyAnalyzerApp(tk.Frame):
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(
@ -104,12 +101,11 @@ class DependencyAnalyzerApp(tk.Frame):
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,
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)
@ -168,38 +164,54 @@ class DependencyAnalyzerApp(tk.Frame):
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."""
"""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,
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)
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)
# 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)
results_scrollbar = ttk.Scrollbar(
results_frame, orient="vertical", command=self.normalizer_results_tree.yview
# 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_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)
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)
@ -209,10 +221,8 @@ class DependencyAnalyzerApp(tk.Frame):
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)
@ -220,14 +230,11 @@ class DependencyAnalyzerApp(tk.Frame):
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.")
@ -237,46 +244,38 @@ class DependencyAnalyzerApp(tk.Frame):
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.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_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()
status = project_normalizer.check_project_status(self.selected_repository_path)
if not all(status.values()):
message = (
if messagebox.askyesno("Project Setup Incomplete",
"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):
"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 _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.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__}...")
@ -284,195 +283,66 @@ class DependencyAnalyzerApp(tk.Frame):
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,
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_to_toggle:
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
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}")
# 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) -> None:
"""Populates the modules treeview with external dependency names."""
def _populate_modules_tree(self):
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."""
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())
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.")
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) -> 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:
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
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()
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 ---
def _normalize_project_threaded(self) -> None:
@ -480,46 +350,53 @@ class DependencyAnalyzerApp(tk.Frame):
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,
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."""
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_results_tree.delete(*self.normalizer_results_tree.get_children())
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",
"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():
for i, (step_key, (success, message)) in enumerate(results.items()):
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(
"",
tk.END,
values=(step_name, status, message),
tags=(status,),
)
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."""
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__":
root = tk.Tk()

View File

@ -13,6 +13,7 @@
# 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, ...)
# sysconfig (Used in: dependencyanalyzer\core\stdlib_detector.py)
# textwrap (Used in: dependencyanalyzer\gui.py)
# threading (Used in: 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, ...)
@ -27,5 +28,6 @@ core
gui
# Found in: dependencyanalyzer\core\package_manager.py
packaging==25.0
# Version not detected in analysis environment.
packaging