# 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