147 lines
6.4 KiB
Python
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}") |