sistemata la funzione di normalizzazione con anche le librerie custom importate in maniera assoluta, rivisitazione della gui

This commit is contained in:
VALLONGOL 2025-11-10 14:43:04 +01:00
parent 6ce186efbb
commit a91820848d
3 changed files with 141 additions and 128 deletions

View File

@ -33,6 +33,10 @@ MODULE_NAME_TO_PACKAGE_NAME_MAP: Dict[str, str] = {
# Modules that are often incorrectly identified as external.
FALSE_POSITIVE_EXTERNAL_MODULES: Set[str] = {"mpl_toolkits"}
# Modules that are known to be local to the project's ecosystem but are
# not in the standard scan path (e.g., sibling directories).
PROJECT_SPECIFIC_LOCAL_MODULES: Set[str] = {"geoelevation"}
class ImportExtractor(ast.NodeVisitor):
"""
@ -163,6 +167,13 @@ def find_project_modules_and_dependencies(
)
continue
# Ignore known project-specific local modules that are not on PyPI
if imp_module in PROJECT_SPECIFIC_LOCAL_MODULES:
logger.info(
f"Skipping known project-specific local module: '{imp_module}'"
)
continue
# Ignore known false positives
if imp_module in FALSE_POSITIVE_EXTERNAL_MODULES:
logger.info(f"Skipping known false positive: '{imp_module}'")

View File

@ -1,7 +1,6 @@
# dependency_analyzer/core/project_normalizer.py
"""
Handles the logic for normalizing a Python project structure.
This includes creating virtual environments, generating configuration files,
and installing dependencies in an isolated manner.
"""
@ -9,201 +8,140 @@ import logging
import subprocess
import sys
from pathlib import Path
from typing import Dict, List, Tuple
from typing import Callable, Dict, List, Optional, Tuple
# Import functions from other core modules that we will reuse
from . import analyzer, package_manager
# --- Logger Configuration ---
logger = logging.getLogger(__name__)
def check_project_status(project_path: Path) -> Dict[str, bool]:
"""
Analyzes a project directory to check for key normalization indicators.
"""
if not project_path or not project_path.is_dir():
return {'has_venv': False, 'has_requirements': False, 'has_pyproject': False}
status = {
'has_venv': (project_path / '.venv').is_dir(),
'has_requirements': (project_path / 'requirements.txt').is_file(),
'has_pyproject': (project_path / 'pyproject.toml').is_file(),
}
status = {'has_venv': (project_path / '.venv').is_dir(),
'has_requirements': (project_path / 'requirements.txt').is_file(),
'has_pyproject': (project_path / 'pyproject.toml').is_file()}
logger.info(f"Project status check for '{project_path.name}': {status}")
return status
def _run_command(
command: List[str], cwd: Path, description: str
) -> Tuple[bool, str]:
"""
A helper to run a shell command and return its status and output.
Args:
command: The command to run as a list of strings.
cwd: The working directory for the command.
description: A brief description of the action for logging.
Returns:
A tuple (success_flag, message).
"""
def _run_command(command: List[str], cwd: Path, description: str) -> Tuple[bool, str]:
logger.info(f"Running task: {description}...")
logger.debug(f"Executing command: {' '.join(command)}")
try:
proc = subprocess.run(
command,
cwd=cwd,
capture_output=True,
text=True,
encoding="utf-8",
errors="ignore",
check=True, # Raise an exception for non-zero exit codes
timeout=600, # 10-minute timeout
command, cwd=cwd, capture_output=True, text=True, encoding="utf-8",
errors="ignore", check=True, timeout=600,
creationflags=(subprocess.CREATE_NO_WINDOW if sys.platform == "win32" else 0),
)
message = f"Task '{description}' completed successfully."
if proc.stdout and proc.stdout.strip():
logger.debug(f"Output from '{description}':\n{proc.stdout.strip()}")
return True, message
output = proc.stdout.strip() or f"Task '{description}' completed successfully."
return True, output
except FileNotFoundError:
msg = f"Error: Command '{command[0]}' not found. Is Python correctly installed and in PATH?"
logger.error(msg)
msg = f"Error: Command '{command[0]}' not found. Is Python in PATH?"
return False, msg
except subprocess.TimeoutExpired:
msg = f"Error: Task '{description}' timed out after 10 minutes."
logger.error(msg)
msg = f"Error: Task '{description}' timed out."
return False, msg
except subprocess.CalledProcessError as e:
error_details = e.stderr.strip() or e.stdout.strip()
msg = f"Error during '{description}':\n{error_details}"
logger.error(msg)
msg = f"Error during '{description}':\n{e.stderr.strip() or e.stdout.strip()}"
return False, msg
except Exception as e:
msg = f"An unexpected error occurred during '{description}': {e}"
logger.exception(msg)
return False, msg
def _get_venv_python_path(project_path: Path) -> Path:
"""
Determines the path to the Python executable inside the .venv directory.
"""
if sys.platform == "win32":
return project_path / ".venv" / "Scripts" / "python.exe"
else:
return project_path / ".venv" / "bin" / "python"
return project_path / ".venv" / ("Scripts" if sys.platform == "win32" else "bin") / "python.exe"
def normalize_project(project_path: Path) -> Dict[str, Tuple[bool, str]]:
def normalize_project(
project_path: Path,
progress_callback: Optional[Callable[[str, bool, str], None]] = None
) -> None:
"""
Executes the full project normalization workflow.
Executes the full project normalization workflow, reporting progress via a callback.
Args:
project_path: The root path of the project to normalize.
Returns:
A dictionary containing the success status and a message for each step.
progress_callback: A function to call with updates. It should accept
(step_key, success_flag, message).
"""
results: Dict[str, Tuple[bool, str]] = {}
def report_progress(step_key: str, success: bool, message: str):
if progress_callback:
progress_callback(step_key, success, message)
# Log to file/console as well
log_level = logging.INFO if success else logging.ERROR
logging.log(log_level, f"Step '{step_key}': {message}")
# --- Step 1: Create Virtual Environment ---
venv_path = project_path / ".venv"
if venv_path.exists():
logger.info("Virtual environment '.venv' already exists. Skipping creation.")
results["venv_creation"] = (True, "Virtual environment '.venv' already exists.")
report_progress("venv_creation", True, "Virtual environment '.venv' already exists.")
else:
success, message = _run_command(
[sys.executable, "-m", "venv", ".venv"],
cwd=project_path,
description="Create virtual environment",
[sys.executable, "-m", "venv", ".venv"], cwd=project_path, description="Create virtual environment"
)
results["venv_creation"] = (success, message)
if not success:
return results # Stop if venv creation fails
report_progress("venv_creation", success, message)
if not success: return
# --- Step 2: Analyze Dependencies ---
logger.info("Analyzing project dependencies using AST...")
try:
# Determine scan path (same logic as in the GUI)
scan_path = project_path
potential_sub_pkg = project_path / project_path.name.lower()
if potential_sub_pkg.is_dir():
scan_path = potential_sub_pkg
if potential_sub_pkg.is_dir(): scan_path = potential_sub_pkg
std_lib_info, external_info = analyzer.find_project_modules_and_dependencies(
repo_path=project_path, scan_path=scan_path
)
results["analysis"] = (True, f"Analysis found {len(external_info)} external dependencies.")
report_progress("analysis", True, f"Analysis found {len(external_info)} external dependencies.")
except Exception as e:
msg = f"Failed to analyze project dependencies: {e}"
logger.exception(msg)
results["analysis"] = (False, msg)
return results
report_progress("analysis", False, f"Failed to analyze project dependencies: {e}")
return
# --- Step 3: Generate requirements.txt ---
logger.info("Generating requirements.txt file...")
try:
req_file = package_manager.generate_requirements_file(
project_path, external_info, std_lib_info
)
results["requirements_generation"] = (True, f"Successfully created '{req_file.name}'.")
report_progress("requirements_generation", True, f"Successfully created '{req_file.name}'.")
except Exception as e:
msg = f"Failed to generate requirements.txt: {e}"
logger.exception(msg)
results["requirements_generation"] = (False, msg)
return results
report_progress("requirements_generation", False, f"Failed to generate requirements.txt: {e}")
return
# --- Step 4: Install Dependencies into .venv ---
if not external_info:
logger.info("No external dependencies to install.")
results["dependency_installation"] = (True, "No external dependencies found to install.")
report_progress("dependency_installation", True, "No external dependencies found to install.")
else:
venv_python = _get_venv_python_path(project_path)
success, message = _run_command(
[str(venv_python), "-m", "pip", "install", "-r", "requirements.txt"],
cwd=project_path,
description="Install dependencies into .venv",
)
results["dependency_installation"] = (success, message)
if not success:
return results # Stop if installation fails
# We now install one by one for better feedback
all_ok = True
error_messages = []
for package_name in sorted(external_info.keys()):
report_progress("dependency_installation", True, f"Installing '{package_name}'...")
success, message = _run_command(
[str(venv_python), "-m", "pip", "install", package_name],
cwd=project_path, description=f"Install {package_name}"
)
if not success:
all_ok = False
error_messages.append(message)
if all_ok:
report_progress("dependency_installation", True, "All dependencies installed successfully.")
else:
final_error = "Failed to install one or more dependencies:\n" + "\n\n".join(error_messages)
report_progress("dependency_installation", False, final_error)
# We don't return here, to allow pyproject.toml creation anyway
# --- Step 5: Create pyproject.toml ---
pyproject_path = project_path / "pyproject.toml"
if pyproject_path.exists():
logger.info("'pyproject.toml' already exists. Skipping creation.")
results["pyproject_creation"] = (True, "'pyproject.toml' already exists.")
report_progress("pyproject_creation", True, "'pyproject.toml' already exists.")
else:
logger.info("Creating a minimal pyproject.toml file...")
project_name = project_path.name.lower().replace("_", "-")
pyproject_content = f"""[build-system]
requires = ["setuptools>=61.0"]
build-backend = "setuptools.build_meta"
[project]
name = "{project_name}"
version = "0.1.0"
description = "A short description of the project."
readme = "README.md"
requires-python = ">=3.8"
# Add other classifiers or dependencies here if needed
# dependencies = [
# "dependency1",
# "dependency2",
# ]
[project.urls]
Homepage = "https://example.com"
"""
pyproject_content = f"""[build-system]\nrequires = ["setuptools>=61.0"]\nbuild-backend = "setuptools.build_meta"\n\n[project]\nname = "{project_name}"\nversion = "0.1.0"\n"""
try:
pyproject_path.write_text(pyproject_content, encoding="utf-8")
msg = "Successfully created a minimal 'pyproject.toml'."
logger.info(msg)
results["pyproject_creation"] = (True, msg)
report_progress("pyproject_creation", True, "Successfully created a minimal 'pyproject.toml'.")
except IOError as e:
msg = f"Failed to write 'pyproject.toml': {e}"
logger.error(msg)
results["pyproject_creation"] = (False, msg)
return results
report_progress("pyproject_creation", False, f"Failed to write 'pyproject.toml': {e}")

View File

@ -344,16 +344,81 @@ class DependencyAnalyzerApp(tk.Frame):
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,
callback_success=self._normalize_project_callback,
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."""
@ -383,9 +448,9 @@ class DependencyAnalyzerApp(tk.Frame):
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
if not selected_indices: return
selected_index = selected_indices[0]
try:
@ -395,7 +460,6 @@ class DependencyAnalyzerApp(tk.Frame):
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__":