add deprecated code for compability
This commit is contained in:
318
deprecated/Resolver/ConflictGrubber.py
Normal file
318
deprecated/Resolver/ConflictGrubber.py
Normal file
@@ -0,0 +1,318 @@
|
||||
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
|
||||
0
deprecated/Resolver/Decider/__init__.py
Normal file
0
deprecated/Resolver/Decider/__init__.py
Normal file
21
deprecated/Resolver/PipConflictGrubber.py
Normal file
21
deprecated/Resolver/PipConflictGrubber.py
Normal file
@@ -0,0 +1,21 @@
|
||||
from pythonapp.Decider.Loader import RequirementInfo
|
||||
from pythonapp.Env.Env import Env
|
||||
from pythonapp.Libs.pip_api import pip_api
|
||||
|
||||
|
||||
class PipConflictGrubber:
|
||||
|
||||
@classmethod
|
||||
def check_for_conflicts(cls,
|
||||
env: Env,
|
||||
installed_reqs: list[RequirementInfo],
|
||||
requested_reqs: list[RequirementInfo],
|
||||
repo_url: str
|
||||
):
|
||||
tmp = [str(req) for req in installed_reqs]
|
||||
tmp.extend([str(req) for req in requested_reqs])
|
||||
|
||||
|
||||
result = pip_api.run_pip_install(env.pip_path, tmp, extra_index_urls=[repo_url])
|
||||
if not result.success:
|
||||
raise RuntimeError(result.stderr)
|
||||
101
deprecated/Resolver/Resolver.py
Normal file
101
deprecated/Resolver/Resolver.py
Normal file
@@ -0,0 +1,101 @@
|
||||
|
||||
import subprocess
|
||||
|
||||
from pythonapp.Env.Env import Env
|
||||
|
||||
|
||||
|
||||
import os
|
||||
from typing import Literal
|
||||
from pathlib import Path
|
||||
|
||||
import subprocess
|
||||
import re
|
||||
from dataclasses import dataclass, field
|
||||
from typing import List, Dict, Set, Tuple, Optional
|
||||
from enum import Enum
|
||||
import logging
|
||||
|
||||
# Настройка логирования
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ConflictSeverity(Enum):
|
||||
FATAL = "Fatal"
|
||||
ERROR = "Error"
|
||||
WARNING = "Warning"
|
||||
|
||||
|
||||
@dataclass
|
||||
class ConflictError:
|
||||
"""Класс для хранения информации о конфликте зависимостей."""
|
||||
conflicting_packages: Set[str]
|
||||
sources: Dict[str, str] # package -> source (installed/requested)
|
||||
severity: ConflictSeverity
|
||||
error_message: str
|
||||
resolution_action: str = ""
|
||||
|
||||
def __post_init__(self):
|
||||
# Автоматически определяем источники, если не предоставлены
|
||||
if not self.sources:
|
||||
self.sources = {pkg: "unknown" for pkg in self.conflicting_packages}
|
||||
|
||||
|
||||
|
||||
class Resolver:
|
||||
|
||||
|
||||
pass
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
"""
|
||||
Класс для поиска и обработки конфликтов зависимостей
|
||||
|
||||
Attributes:
|
||||
req_list (list[RequirementInfo])
|
||||
Список установленных пакетов в системе
|
||||
|
||||
requirement_str (str):
|
||||
Полная строка требования как в файле requirements.txt.
|
||||
Пример: "requests>=2.25.0" или "numpy==1.21.0".
|
||||
|
||||
package_name (str):
|
||||
Имя пакета без указания версии.
|
||||
Пример: "requests" или "numpy".
|
||||
|
||||
significance_level (Literal["manual", "required", "optional"]):
|
||||
Уровень значимости требования по убыванию важности:
|
||||
- "manual": пакеты, установленные вручную
|
||||
- "required": обязательные зависимости
|
||||
- "optional": опциональные зависимости
|
||||
|
||||
source_file (str):
|
||||
Имя файла требований, из которого было извлечено это требование.
|
||||
Пример: "core.req" или "dev.opt".
|
||||
"""
|
||||
|
||||
"""
|
||||
Теперь давай разработаем статический класс Resolver и его метод check integrity. Приведенного ниже алгоритма есть проблема, что он может игнорировать некоторые опциональное зависимости, но не сообщает об этом коду, который далее будет устанавливать пакеты и не удаляет их из self.config.requirements_dir.
|
||||
ВНИМАНИЕ!!! Этот код не должен менять сами файлы. Он может только удалять пакеты из своих временных списков, но не из файлов. Также он должен быть полностью инкапсулирован от класса Instance. Все необходимые для его работы данные должны быть переданы непосредственно при вызове метода check_integrity. Данные класса instance, ровно, как и принадлежащие ему файлы не должны быть изменены в результате работы. Все что делает этот код - получает исходные данные и на их основе принимает решение и возвращает ответ, никак не трогая сами исходные данные.
|
||||
В соответствии с этим классом должен быть изменен метод check_integrity класса Instance, но ничего более.
|
||||
Метод всегда должен возвращать три переменных: список оставшихся пакетов, список ошибок на req, список ошибок на opt. Переделай check integrity класса instance с учетом этого.
|
||||
Также pip dry run должен проводиться в venv окружении self.config.test_venv
|
||||
его алгоритм:
|
||||
|
||||
|
||||
ЭТАП Б: Обработка ошибок
|
||||
1. Пытается запустить pip --dry-run со всеми пакетами из обоих списков. Если ошибок нет, возвращает true
|
||||
2. Если ошибки есть, то парсит вывод pip, находит пакет, вызвавший конфликт в списке запрашиваемых пакетов, и пакет, который конфликтует с ним в словарях req и opt,
|
||||
3. Если пакет найден в opt и флаг required == True, удаляет его из словарей списков. Если пакет найден в req или флаг required == False, удаляет пакет из списка запрашиваемых
|
||||
4. Заново формирует общие списки.
|
||||
5. Переносит имя нового и установленного конфликтующих пакетов в списки err_req и err_opt dataclass ErrorDesc, где содержится их имена с версиями, а также полный текст ошибки pip.
|
||||
6. Повторяет процесс обработки ошибок, пока ошибок не останется.
|
||||
ЭТАП В: Подведение итогов
|
||||
1. Если флаг interactive, то выводим список ошибок, спрашиваем у пользователя, продолжать или нет.
|
||||
2. Если флаг required == False, возвращаем список оставшихся запрашиваемых пакетов
|
||||
3. Иначе если список запрашиваемых пакетов остался неизменным, возвращаем его, если нет, исключение - неразрешимый конфликт.
|
||||
"""
|
||||
0
deprecated/Resolver/__init__.py
Normal file
0
deprecated/Resolver/__init__.py
Normal file
Reference in New Issue
Block a user