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. # Modules that are often incorrectly identified as external.
FALSE_POSITIVE_EXTERNAL_MODULES: Set[str] = {"mpl_toolkits"} 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): class ImportExtractor(ast.NodeVisitor):
""" """
@ -163,6 +167,13 @@ def find_project_modules_and_dependencies(
) )
continue 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 # Ignore known false positives
if imp_module in FALSE_POSITIVE_EXTERNAL_MODULES: if imp_module in FALSE_POSITIVE_EXTERNAL_MODULES:
logger.info(f"Skipping known false positive: '{imp_module}'") logger.info(f"Skipping known false positive: '{imp_module}'")

View File

@ -1,7 +1,6 @@
# dependency_analyzer/core/project_normalizer.py # dependency_analyzer/core/project_normalizer.py
""" """
Handles the logic for normalizing a Python project structure. Handles the logic for normalizing a Python project structure.
This includes creating virtual environments, generating configuration files, This includes creating virtual environments, generating configuration files,
and installing dependencies in an isolated manner. and installing dependencies in an isolated manner.
""" """
@ -9,201 +8,140 @@ import logging
import subprocess import subprocess
import sys import sys
from pathlib import Path 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 from . import analyzer, package_manager
# --- Logger Configuration ---
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
def check_project_status(project_path: Path) -> Dict[str, bool]: 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(): if not project_path or not project_path.is_dir():
return {'has_venv': False, 'has_requirements': False, 'has_pyproject': False} return {'has_venv': False, 'has_requirements': False, 'has_pyproject': False}
status = {'has_venv': (project_path / '.venv').is_dir(),
status = { 'has_requirements': (project_path / 'requirements.txt').is_file(),
'has_venv': (project_path / '.venv').is_dir(), 'has_pyproject': (project_path / 'pyproject.toml').is_file()}
'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}") logger.info(f"Project status check for '{project_path.name}': {status}")
return status return status
def _run_command( def _run_command(command: List[str], cwd: Path, description: str) -> Tuple[bool, str]:
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).
"""
logger.info(f"Running task: {description}...") logger.info(f"Running task: {description}...")
logger.debug(f"Executing command: {' '.join(command)}") logger.debug(f"Executing command: {' '.join(command)}")
try: try:
proc = subprocess.run( proc = subprocess.run(
command, command, cwd=cwd, capture_output=True, text=True, encoding="utf-8",
cwd=cwd, errors="ignore", check=True, timeout=600,
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
creationflags=(subprocess.CREATE_NO_WINDOW if sys.platform == "win32" else 0), creationflags=(subprocess.CREATE_NO_WINDOW if sys.platform == "win32" else 0),
) )
message = f"Task '{description}' completed successfully." output = proc.stdout.strip() or f"Task '{description}' completed successfully."
if proc.stdout and proc.stdout.strip(): return True, output
logger.debug(f"Output from '{description}':\n{proc.stdout.strip()}")
return True, message
except FileNotFoundError: except FileNotFoundError:
msg = f"Error: Command '{command[0]}' not found. Is Python correctly installed and in PATH?" msg = f"Error: Command '{command[0]}' not found. Is Python in PATH?"
logger.error(msg)
return False, msg return False, msg
except subprocess.TimeoutExpired: except subprocess.TimeoutExpired:
msg = f"Error: Task '{description}' timed out after 10 minutes." msg = f"Error: Task '{description}' timed out."
logger.error(msg)
return False, msg return False, msg
except subprocess.CalledProcessError as e: except subprocess.CalledProcessError as e:
error_details = e.stderr.strip() or e.stdout.strip() msg = f"Error during '{description}':\n{e.stderr.strip() or e.stdout.strip()}"
msg = f"Error during '{description}':\n{error_details}"
logger.error(msg)
return False, msg return False, msg
except Exception as e: except Exception as e:
msg = f"An unexpected error occurred during '{description}': {e}" msg = f"An unexpected error occurred during '{description}': {e}"
logger.exception(msg)
return False, msg return False, msg
def _get_venv_python_path(project_path: Path) -> Path: def _get_venv_python_path(project_path: Path) -> Path:
""" return project_path / ".venv" / ("Scripts" if sys.platform == "win32" else "bin") / "python.exe"
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"
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: Args:
project_path: The root path of the project to normalize. project_path: The root path of the project to normalize.
progress_callback: A function to call with updates. It should accept
Returns: (step_key, success_flag, message).
A dictionary containing the success status and a message for each step.
""" """
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 --- # --- Step 1: Create Virtual Environment ---
venv_path = project_path / ".venv" venv_path = project_path / ".venv"
if venv_path.exists(): if venv_path.exists():
logger.info("Virtual environment '.venv' already exists. Skipping creation.") report_progress("venv_creation", True, "Virtual environment '.venv' already exists.")
results["venv_creation"] = (True, "Virtual environment '.venv' already exists.")
else: else:
success, message = _run_command( success, message = _run_command(
[sys.executable, "-m", "venv", ".venv"], [sys.executable, "-m", "venv", ".venv"], cwd=project_path, description="Create virtual environment"
cwd=project_path,
description="Create virtual environment",
) )
results["venv_creation"] = (success, message) report_progress("venv_creation", success, message)
if not success: if not success: return
return results # Stop if venv creation fails
# --- Step 2: Analyze Dependencies --- # --- Step 2: Analyze Dependencies ---
logger.info("Analyzing project dependencies using AST...")
try: try:
# Determine scan path (same logic as in the GUI)
scan_path = project_path scan_path = project_path
potential_sub_pkg = project_path / project_path.name.lower() potential_sub_pkg = project_path / project_path.name.lower()
if potential_sub_pkg.is_dir(): if potential_sub_pkg.is_dir(): scan_path = potential_sub_pkg
scan_path = potential_sub_pkg
std_lib_info, external_info = analyzer.find_project_modules_and_dependencies( std_lib_info, external_info = analyzer.find_project_modules_and_dependencies(
repo_path=project_path, scan_path=scan_path 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: except Exception as e:
msg = f"Failed to analyze project dependencies: {e}" report_progress("analysis", False, f"Failed to analyze project dependencies: {e}")
logger.exception(msg) return
results["analysis"] = (False, msg)
return results
# --- Step 3: Generate requirements.txt --- # --- Step 3: Generate requirements.txt ---
logger.info("Generating requirements.txt file...")
try: try:
req_file = package_manager.generate_requirements_file( req_file = package_manager.generate_requirements_file(
project_path, external_info, std_lib_info 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: except Exception as e:
msg = f"Failed to generate requirements.txt: {e}" report_progress("requirements_generation", False, f"Failed to generate requirements.txt: {e}")
logger.exception(msg) return
results["requirements_generation"] = (False, msg)
return results
# --- Step 4: Install Dependencies into .venv --- # --- Step 4: Install Dependencies into .venv ---
if not external_info: if not external_info:
logger.info("No external dependencies to install.") report_progress("dependency_installation", True, "No external dependencies found to install.")
results["dependency_installation"] = (True, "No external dependencies found to install.")
else: else:
venv_python = _get_venv_python_path(project_path) venv_python = _get_venv_python_path(project_path)
success, message = _run_command( # We now install one by one for better feedback
[str(venv_python), "-m", "pip", "install", "-r", "requirements.txt"], all_ok = True
cwd=project_path, error_messages = []
description="Install dependencies into .venv", for package_name in sorted(external_info.keys()):
) report_progress("dependency_installation", True, f"Installing '{package_name}'...")
results["dependency_installation"] = (success, message) success, message = _run_command(
if not success: [str(venv_python), "-m", "pip", "install", package_name],
return results # Stop if installation fails 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 --- # --- Step 5: Create pyproject.toml ---
pyproject_path = project_path / "pyproject.toml" pyproject_path = project_path / "pyproject.toml"
if pyproject_path.exists(): if pyproject_path.exists():
logger.info("'pyproject.toml' already exists. Skipping creation.") report_progress("pyproject_creation", True, "'pyproject.toml' already exists.")
results["pyproject_creation"] = (True, "'pyproject.toml' already exists.")
else: else:
logger.info("Creating a minimal pyproject.toml file...")
project_name = project_path.name.lower().replace("_", "-") project_name = project_path.name.lower().replace("_", "-")
pyproject_content = f"""[build-system] 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"""
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"
"""
try: try:
pyproject_path.write_text(pyproject_content, encoding="utf-8") pyproject_path.write_text(pyproject_content, encoding="utf-8")
msg = "Successfully created a minimal 'pyproject.toml'." report_progress("pyproject_creation", True, "Successfully created a minimal 'pyproject.toml'.")
logger.info(msg)
results["pyproject_creation"] = (True, msg)
except IOError as e: except IOError as e:
msg = f"Failed to write 'pyproject.toml': {e}" report_progress("pyproject_creation", False, f"Failed to write 'pyproject.toml': {e}")
logger.error(msg)
results["pyproject_creation"] = (False, msg)
return results

View File

@ -344,16 +344,81 @@ class DependencyAnalyzerApp(tk.Frame):
logging.log(logging.INFO if success else logging.ERROR, f"{task_name}: {message}") logging.log(logging.INFO if success else logging.ERROR, f"{task_name}: {message}")
if not success: messagebox.showerror(f"{task_name} Failed", message) if not success: messagebox.showerror(f"{task_name} Failed", message)
# --- Methods for "Project Normalizer" Tab ---
# --- Methods for "Project Normalizer" Tab --- # --- Methods for "Project Normalizer" Tab ---
def _normalize_project_threaded(self) -> None: def _normalize_project_threaded(self) -> None:
"""Starts the project normalization process in a background thread.""" """Starts the project normalization process in a background thread."""
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_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( self._run_long_task_threaded(
project_normalizer.normalize_project, self.selected_repository_path, project_normalizer.normalize_project,
callback_success=self._normalize_project_callback, 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: def _normalize_project_callback(self, results: Dict[str, Tuple[bool, str]]) -> None:
"""Handles the results of the normalization, populating the new UI.""" """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: def _on_normalizer_step_select(self, event: Optional[tk.Event]) -> None:
"""Displays the details for the currently selected normalization step.""" """Displays the details for the currently selected normalization step."""
# This function remains largely the same
selected_indices = self.normalizer_steps_list.curselection() selected_indices = self.normalizer_steps_list.curselection()
if not selected_indices: if not selected_indices: return
return
selected_index = selected_indices[0] selected_index = selected_indices[0]
try: try:
@ -395,7 +460,6 @@ class DependencyAnalyzerApp(tk.Frame):
self.normalizer_details_text.insert('1.0', message) self.normalizer_details_text.insert('1.0', message)
self.normalizer_details_text.config(state=tk.DISABLED) self.normalizer_details_text.config(state=tk.DISABLED)
except IndexError: except IndexError:
# This should not happen, but it's a safe fallback
pass pass
if __name__ == "__main__": if __name__ == "__main__":