SXXXXXXX_DependencyAnalyzer/dependencyanalyzer/core/project_normalizer.py
2025-11-10 14:18:35 +01:00

209 lines
7.5 KiB
Python

# 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.
"""
import logging
import subprocess
import sys
from pathlib import Path
from typing import Dict, List, 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(),
}
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).
"""
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
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
except FileNotFoundError:
msg = f"Error: Command '{command[0]}' not found. Is Python correctly installed and in PATH?"
logger.error(msg)
return False, msg
except subprocess.TimeoutExpired:
msg = f"Error: Task '{description}' timed out after 10 minutes."
logger.error(msg)
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)
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"
def normalize_project(project_path: Path) -> Dict[str, Tuple[bool, str]]:
"""
Executes the full project normalization workflow.
Args:
project_path: The root path of the project to normalize.
Returns:
A dictionary containing the success status and a message for each step.
"""
results: Dict[str, Tuple[bool, str]] = {}
# --- 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.")
else:
success, message = _run_command(
[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
# --- 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
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.")
except Exception as e:
msg = f"Failed to analyze project dependencies: {e}"
logger.exception(msg)
results["analysis"] = (False, msg)
return results
# --- 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}'.")
except Exception as e:
msg = f"Failed to generate requirements.txt: {e}"
logger.exception(msg)
results["requirements_generation"] = (False, msg)
return results
# --- 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.")
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
# --- 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.")
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"
"""
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)
except IOError as e:
msg = f"Failed to write 'pyproject.toml': {e}"
logger.error(msg)
results["pyproject_creation"] = (False, msg)
return results