add deprecated code for compability

This commit is contained in:
Bacruru Sakaguchi
2025-09-12 17:18:13 +07:00
parent 9e5e214944
commit 9651175e9a
12 changed files with 1115 additions and 0 deletions

View 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

View File

View 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)

View 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. Иначе если список запрашиваемых пакетов остался неизменным, возвращаем его, если нет, исключение - неразрешимый конфликт.
"""

View File