Duibonduil's picture
Upload 5 files
4e067f2 verified
# 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")