Files
vaiola/deprecated/Resolver/ConflictGrubber.py
2025-09-12 17:18:13 +07:00

318 lines
14 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import subprocess
import re
from dataclasses import dataclass, field
from typing import List, Dict, Set, Optional
from enum import Enum
import logging
from pythonapp.Decider.Loader import RequirementInfo
# Настройка логирования
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
@dataclass
class ConflictGrubber:
"""Класс для разрешения конфликтов зависимостей."""
conflict_errors: List[ConflictError] = field(default_factory=list)
max_attempts: int = 10
def check_for_conflicts(self,
env,
installed_reqs: List[RequirementInfo],
requested_reqs: List[RequirementInfo],
repo_url: str) -> List[ConflictError]:
"""
Проверяет конфликты зависимостей между установленными и запрашиваемыми пакетами.
Args:
env: Виртуальное окружение
installed_reqs: Список установленных пакетов
requested_reqs: Список запрашиваемых пакетов
repo_url: URL репозитория пакетов
Returns:
Список ошибок конфликтов
"""
self.conflict_errors.clear()
current_requested = requested_reqs.copy()
attempt = 0
# Создаем словари для быстрого доступа
installed_dict = {req.package_name: req for req in installed_reqs}
requested_dict = {req.package_name: req for req in current_requested}
while attempt < self.max_attempts:
attempt += 1
logger.info(f"Попытка разрешения конфликтов #{attempt}")
# Формируем команду для dry-run
all_requirements = [req.requirement_str for req in installed_reqs + current_requested]
# Выполняем dry-run установки
result = self._run_dry_install(env, all_requirements, repo_url)
# Анализируем результат
errors = self._parse_pip_output(result.stderr, result.stdout)
if not errors:
logger.info("Конфликты не обнаружены")
break
# Обрабатываем ошибки
if not self._process_errors(errors, installed_dict, requested_dict, current_requested):
logger.warning("Обнаружены неустранимые конфликты")
break
return self.conflict_errors
def _run_dry_install(self, env, requirements: List[str], repo_url: str) -> subprocess.CompletedProcess:
"""Выполняет dry-run установки пакетов."""
command = [env.pip_path, "install", "--dry-run", "--no-cache-dir"]
command.extend(requirements)
if repo_url:
command.extend(["--extra-index-url", repo_url])
logger.debug(f"Выполнение команды: {' '.join(command)}")
return subprocess.run(command, capture_output=True, text=True, timeout=300)
def _parse_pip_output(self, stderr: str, stdout: str) -> List[Dict]:
"""Анализирует вывод pip и извлекает информацию о конфликтах."""
errors = []
output = stderr + stdout
# Ищем конфликты версий
version_conflicts = re.findall(
r'ERROR: Cannot install ([^\n]+) because these package versions have conflicting dependencies',
output
)
for conflict in version_conflicts:
packages = re.findall(r'([a-zA-Z0-9_\-\.]+)==([a-zA-Z0-9_\-\.]+)', conflict)
if packages:
error_info = {
'type': 'version_conflict',
'packages': {pkg: ver for pkg, ver in packages},
'message': f"Конфликт версий: {conflict}"
}
errors.append(error_info)
# Ищем несовместимые требования
requirement_conflicts = re.findall(
r'ERROR: ([a-zA-Z0-9_\-\.]+) ([^\n]+) has requirement ([^\n]+), but you\'ll have ([^\n]+) which is incompatible',
output
)
for pkg, version, required, actual in requirement_conflicts:
error_info = {
'type': 'requirement_conflict',
'package': pkg.strip(),
'required': required.strip(),
'actual': actual.strip(),
'message': f"{pkg} {version} требует {required}, но установлена {actual}"
}
errors.append(error_info)
return errors
def _process_errors(self, errors: List[Dict],
installed_dict: Dict[str, RequirementInfo],
requested_dict: Dict[str, RequirementInfo],
current_requested: List[RequirementInfo]) -> bool:
"""Обрабатывает ошибки и пытается их разрешить."""
resolved = True
for error in errors:
if error['type'] == 'version_conflict':
conflict_resolved = self._handle_version_conflict(
error, installed_dict, requested_dict, current_requested
)
resolved = resolved and conflict_resolved
elif error['type'] == 'requirement_conflict':
conflict_resolved = self._handle_requirement_conflict(
error, installed_dict, requested_dict, current_requested
)
resolved = resolved and conflict_resolved
return resolved
def _handle_version_conflict(self, error: Dict,
installed_dict: Dict[str, RequirementInfo],
requested_dict: Dict[str, RequirementInfo],
current_requested: List[RequirementInfo]) -> bool:
"""Обрабатывает конфликт версий."""
conflicting_packages = set(error['packages'].keys())
sources = {}
severity = None
# Определяем источники пакетов и их значимость
for pkg in conflicting_packages:
if pkg in installed_dict:
sources[pkg] = 'installed'
elif pkg in requested_dict:
sources[pkg] = 'requested'
else:
sources[pkg] = 'unknown'
# Определяем серьезность конфликта
installed_conflicts = [pkg for pkg in conflicting_packages if sources[pkg] == 'installed']
requested_conflicts = [pkg for pkg in conflicting_packages if sources[pkg] == 'requested']
if len(installed_conflicts) >= 2:
severity = ConflictSeverity.FATAL
elif installed_conflicts and requested_conflicts:
# Проверяем значимость пакетов
installed_severity = max(
installed_dict[pkg].significance_level for pkg in installed_conflicts
)
requested_severity = max(
requested_dict[pkg].significance_level for pkg in requested_conflicts
)
if installed_severity == 'required' and requested_severity == 'required':
severity = ConflictSeverity.FATAL
elif installed_severity == 'required' or requested_severity == 'required':
severity = ConflictSeverity.ERROR
else:
severity = ConflictSeverity.WARNING
else:
# Все пакеты запрашиваемые
severities = [requested_dict[pkg].significance_level for pkg in requested_conflicts]
if 'required' in severities:
severity = ConflictSeverity.ERROR
else:
severity = ConflictSeverity.WARNING
# Создаем запись об ошибке
conflict_error = ConflictError(
conflicting_packages=conflicting_packages,
sources=sources,
severity=severity,
error_message=error['message']
)
self.conflict_errors.append(conflict_error)
# Пытаемся разрешить конфликт
if severity == ConflictSeverity.ERROR:
# Удаляем опциональные пакеты из запрашиваемых
optional_packages = [
pkg for pkg in requested_conflicts
if requested_dict[pkg].significance_level == 'optional'
]
if optional_packages:
for pkg in optional_packages:
self._remove_package(pkg, requested_dict, current_requested)
conflict_error.resolution_action = f"Удален опциональный пакет {pkg}"
return True
elif severity == ConflictSeverity.WARNING:
# Удаляем один из опциональных пакетов
if requested_conflicts:
pkg_to_remove = requested_conflicts[0]
self._remove_package(pkg_to_remove, requested_dict, current_requested)
conflict_error.resolution_action = f"Удален опциональный пакет {pkg_to_remove}"
return True
# Неустранимый конфликт
conflict_error.resolution_action = "Требует ручного вмешательства"
return False
def _handle_requirement_conflict(self, error: Dict,
installed_dict: Dict[str, RequirementInfo],
requested_dict: Dict[str, RequirementInfo],
current_requested: List[RequirementInfo]) -> bool:
"""Обрабатывает конфликт требований."""
pkg = error['package']
sources = {pkg: 'requested' if pkg in requested_dict else 'installed'}
severity = None
# Определяем серьезность конфликта
if pkg in installed_dict:
pkg_severity = installed_dict[pkg].significance_level
else:
pkg_severity = requested_dict[pkg].significance_level
if pkg_severity == 'required':
severity = ConflictSeverity.ERROR
else:
severity = ConflictSeverity.WARNING
# Создаем запись об ошибке
conflict_error = ConflictError(
conflicting_packages={pkg},
sources=sources,
severity=severity,
error_message=error['message']
)
self.conflict_errors.append(conflict_error)
# Пытаемся разрешить конфликт
if severity == ConflictSeverity.ERROR and pkg in requested_dict:
# Для требуемых пакетов пытаемся найти совместимую версию
compatible = self._find_compatible_version(pkg, error['required'], requested_dict)
if compatible:
self._update_package_version(pkg, compatible, requested_dict, current_requested)
conflict_error.resolution_action = f"Обновлена версия {pkg} до {compatible}"
return True
else:
# Не удалось найти совместимую версию
conflict_error.resolution_action = "Не удалось найти совместимую версию"
return False
elif severity == ConflictSeverity.WARNING and pkg in requested_dict:
# Удаляем опциональный пакет
self._remove_package(pkg, requested_dict, current_requested)
conflict_error.resolution_action = f"Удален опциональный пакет {pkg}"
return True
return False
def _remove_package(self, package_name: str,
requested_dict: Dict[str, RequirementInfo],
current_requested: List[RequirementInfo]):
"""Удаляет пакет из списка запрашиваемых."""
if package_name in requested_dict:
package_to_remove = requested_dict[package_name]
current_requested.remove(package_to_remove)
del requested_dict[package_name]
logger.info(f"Удален пакет: {package_name}")
def _update_package_version(self, package_name: str, new_version: str,
requested_dict: Dict[str, RequirementInfo],
current_requested: List[RequirementInfo]):
"""Обновляет версию пакета в списке запрашиваемых."""
if package_name in requested_dict:
old_req = requested_dict[package_name]
new_req_str = f"{package_name}{new_version}"
# Создаем новый RequirementInfo с обновленной версией
new_req = RequirementInfo.from_requirement_string(
new_req_str, old_req.significance_level, old_req.source_file
)
# Заменяем старый запрос на новый
index = current_requested.index(old_req)
current_requested[index] = new_req
requested_dict[package_name] = new_req
logger.info(f"Обновлена версия {package_name} до {new_version}")
def _find_compatible_version(self, package_name: str, requirement: str,
requested_dict: Dict[str, RequirementInfo]) -> Optional[str]:
"""Пытается найти совместимую версию пакета."""
# В реальной реализации здесь должен быть вызов к API PyPI или использование
# библиотеки для разрешения зависимостей, например, pip-tools или poetry
# Это упрощенная реализация
logger.warning(f"Поиск совместимой версии для {package_name} не реализован")
return None