sistemata la funzione di normalizzazione con anche le librerie custom importate in maniera assoluta, rivisitazione della gui
This commit is contained in:
parent
6ce186efbb
commit
a91820848d
@ -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}'")
|
||||
|
||||
@ -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}")
|
||||
@ -344,17 +344,82 @@ 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."""
|
||||
logging.info("Normalization process finished. Displaying results.")
|
||||
@ -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__":
|
||||
|
||||
Loading…
Reference in New Issue
Block a user