# coding: utf-8 # Copyright (c) 2025 inclusionAI. import os.path import time import sys import importlib import subprocess from importlib import metadata from aworld.logs.util import logger class ModuleAlias: def __init__(self, module): self.module = module def __getattr__(self, name): return getattr(self.module, name) def is_package_installed(package_name: str, version: str = "") -> bool: """ Check if package is already installed and matches version if specified. Args: package_name: Name of the package to check version: Required version of the package Returns: bool: True if package is installed (and version matches if specified), False otherwise """ try: dist = metadata.distribution(package_name) if version and dist.version != version: logger.info(f"Package {package_name} is installed but version {dist.version} " f"does not match required version {version}") return False logger.info(f"Package {package_name} is already installed (version: {dist.version})") return True except metadata.PackageNotFoundError: logger.info(f"Package {package_name} is not installed") return False except Exception as e: logger.warning(f"Error checking if {package_name} is installed: {str(e)}") return False def import_packages(packages: list[str]) -> dict: """ Import and install multiple packages Args: packages: List of packages to import Returns: dict: Dictionary mapping package names to imported modules """ modules = {} for package in packages: package_ = import_package(package) if package_: modules[package] = package_ return modules def import_package( package_name: str, alias: str = '', install_name: str = '', version: str = '', installer: str = 'pip', timeout: int = 300, retry_count: int = 3, retry_delay: int = 5 ) -> object: """ Import and install package if not available. Args: package_name: Name of the package to import alias: Alias to use for the imported module install_name: Name of the package to install (if different from import name) version: Required version of the package installer: Package installer to use ('pip' or 'conda') timeout: Installation timeout in seconds retry_count: Number of installation retries if install fails retry_delay: Delay between retries in seconds Returns: Imported module Raises: ValueError: If input parameters are invalid ImportError: If package cannot be imported or installed TimeoutError: If installation exceeds timeout """ # Validate input parameters if not package_name: raise ValueError("Package name cannot be empty") if installer not in ['pip', 'conda']: raise ValueError(f"Unsupported installer: {installer}") # Use package_name as install_name if not provided real_install_name = install_name if install_name else package_name # First, check if we need to install the package need_install = False # Try to import the module first try: logger.debug(f"Attempting to import {package_name}") module = importlib.import_module(package_name) logger.debug(f"Successfully imported {package_name}") # If we successfully imported the module, check version if specified if version: try: # For packages with different import and install names, # we need to check the install name for version info installed_version = metadata.version(real_install_name) if installed_version != version: logger.warning( f"Package {real_install_name} version mismatch. " f"Required: {version}, Installed: {installed_version}" ) need_install = True except metadata.PackageNotFoundError: logger.warning(f"Could not determine version for {real_install_name}") # If no need to reinstall for version mismatch, return the module if not need_install: return ModuleAlias(module) if alias else module except ImportError as import_err: logger.info(f"Could not import {package_name}: {str(import_err)}") # Check if the package is installed if not is_package_installed(real_install_name, version): need_install = True else: # If package is installed but import failed, there might be an issue with dependencies # or the package itself. Still, let's try to reinstall it. logger.warning(f"Package {real_install_name} is installed but import of {package_name} failed. " f"Will attempt reinstallation.") need_install = True # Install the package if needed if need_install: logger.info(f"Installation needed for {real_install_name}") # Attempt installation with retries for attempt in range(retry_count): try: cmd = _get_install_command(installer, real_install_name, version) logger.info(f"Installing {real_install_name} with command: {' '.join(cmd)}") _execute_install_command(cmd, timeout) # Break out of retry loop if installation succeeds break except (ImportError, TimeoutError, subprocess.SubprocessError) as e: if attempt < retry_count - 1: logger.warning( f"Installation attempt {attempt + 1} failed: {str(e)}. Retrying in {retry_delay} seconds...") time.sleep(retry_delay) else: logger.error(f"All installation attempts failed for {real_install_name}") raise ImportError(f"Failed to install {real_install_name} after {retry_count} attempts: {str(e)}") # Try importing after installation try: logger.debug(f"Attempting to import {package_name} after installation") module = importlib.import_module(package_name) logger.debug(f"Successfully imported {package_name}") return ModuleAlias(module) if alias else module except ImportError as e: error_msg = f"Failed to import {package_name} even after installation of {real_install_name}: {str(e)}" logger.error(error_msg) def _get_install_command(installer: str, package_name: str, version: str = "") -> list: """ Generate installation command based on specified installer. Args: installer: Package installer to use ('pip' or 'conda') package_name: Name of the package to install version: Required version of the package Returns: list: Command as a list of strings Raises: ValueError: If unsupported installer is specified """ if installer == 'pip': # Use sys.executable to ensure the right Python interpreter is used pytho3 = os.path.basename(sys.executable) cmd = [sys.executable, '-m', 'pip', 'install', '--upgrade'] if version: cmd.append(f'{package_name}=={version}') else: cmd.append(package_name) elif installer == 'conda': cmd = ['conda', 'install', '-y', package_name] if version: cmd.extend([f'={version}']) else: raise ValueError(f"Unsupported installer: {installer}") return cmd def _execute_install_command(cmd: list, timeout: int) -> None: """ Execute package installation command. Args: cmd: Installation command as list of strings timeout: Installation timeout in seconds Raises: TimeoutError: If installation exceeds timeout ImportError: If installation fails """ logger.info(f"Executing: {' '.join(cmd)}") process = subprocess.Popen( cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE ) try: stdout, stderr = process.communicate(timeout=timeout) # Log installation output for debugging if stdout: logger.debug(f"Installation stdout: {stdout.decode()}") if stderr: logger.debug(f"Installation stderr: {stderr.decode()}") except subprocess.TimeoutExpired: process.kill() error_msg = f"Package installation timed out after {timeout} seconds" logger.error(error_msg) raise TimeoutError(error_msg) if process.returncode != 0: error_msg = f"Installation failed with code {process.returncode}: {stderr.decode()}" logger.error(error_msg) raise ImportError(error_msg) logger.info("Installation completed successfully")