SXXXXXXX_DependencyAnalyzer/dependencyanalyzer/core/project_normalizer.py

147 lines
6.4 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 Callable, Dict, List, Optional, Tuple
from . import analyzer, package_manager
logger = logging.getLogger(__name__)
def check_project_status(project_path: Path) -> Dict[str, bool]:
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]:
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, timeout=600,
creationflags=(subprocess.CREATE_NO_WINDOW if sys.platform == "win32" else 0),
)
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 in PATH?"
return False, msg
except subprocess.TimeoutExpired:
msg = f"Error: Task '{description}' timed out."
return False, msg
except subprocess.CalledProcessError as e:
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}"
return False, msg
def _get_venv_python_path(project_path: Path) -> Path:
return project_path / ".venv" / ("Scripts" if sys.platform == "win32" else "bin") / "python.exe"
def normalize_project(
project_path: Path,
progress_callback: Optional[Callable[[str, bool, str], None]] = None
) -> None:
"""
Executes the full project normalization workflow, reporting progress via a callback.
Args:
project_path: The root path of the project to normalize.
progress_callback: A function to call with updates. It should accept
(step_key, success_flag, message).
"""
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():
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"
)
report_progress("venv_creation", success, message)
if not success: return
# --- Step 2: Analyze Dependencies ---
try:
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
)
report_progress("analysis", True, f"Analysis found {len(external_info)} external dependencies.")
except Exception as e:
report_progress("analysis", False, f"Failed to analyze project dependencies: {e}")
return
# --- Step 3: Generate requirements.txt ---
try:
req_file = package_manager.generate_requirements_file(
project_path, external_info, std_lib_info
)
report_progress("requirements_generation", True, f"Successfully created '{req_file.name}'.")
except Exception as e:
report_progress("requirements_generation", False, f"Failed to generate requirements.txt: {e}")
return
# --- Step 4: Install Dependencies into .venv ---
if not external_info:
report_progress("dependency_installation", True, "No external dependencies found to install.")
else:
venv_python = _get_venv_python_path(project_path)
# 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():
report_progress("pyproject_creation", True, "'pyproject.toml' already exists.")
else:
project_name = project_path.name.lower().replace("_", "-")
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")
report_progress("pyproject_creation", True, "Successfully created a minimal 'pyproject.toml'.")
except IOError as e:
report_progress("pyproject_creation", False, f"Failed to write 'pyproject.toml': {e}")