190 lines
12 KiB
Python
190 lines
12 KiB
Python
|
||
import os
|
||
from typing import Literal
|
||
from pathlib import Path
|
||
from dataclasses import dataclass
|
||
|
||
|
||
@dataclass
|
||
class RequirementInfo:
|
||
"""
|
||
Класс для представления информации о требовании к пакету.
|
||
|
||
Attributes:
|
||
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".
|
||
"""
|
||
requirement_str: str
|
||
package_name: str
|
||
significance_level: Literal["manual", "required", "optional"]
|
||
source_file: str
|
||
|
||
@classmethod
|
||
def from_requirement_string(cls, requirement_str: str,
|
||
significance_level: Literal["manual", "required", "optional"],
|
||
source_file: str) -> "RequirementInfo | None":
|
||
"""
|
||
Создает экземпляр RequirementInfo из строки требования.
|
||
|
||
Args:
|
||
requirement_str: Полная строка требования
|
||
significance_level: Уровень значимости требования
|
||
source_file: Имя файла-источника требования
|
||
|
||
Returns:
|
||
Экземпляр RequirementInfo с извлеченным именем пакета
|
||
"""
|
||
# Извлекаем имя пакета (все до первого символа сравнения или пробела)
|
||
if requirement_str == '': return None
|
||
package_name = requirement_str.split()[0].strip()
|
||
for comparison_op in ["==", ">=", "<=", ">", "<", "!=", "~="]:
|
||
if comparison_op in package_name:
|
||
package_name = package_name.split(comparison_op)[0].strip()
|
||
break
|
||
|
||
return cls(
|
||
requirement_str=requirement_str,
|
||
package_name=package_name,
|
||
significance_level=significance_level,
|
||
source_file=Path(source_file).name # Сохраняем только имя файла, не полный путь
|
||
)
|
||
|
||
def __str__(self) -> str:
|
||
"""Строковое представление требования."""
|
||
return self.requirement_str
|
||
|
||
|
||
class Loader:
|
||
@classmethod
|
||
def _load_req_file(cls, path: str | Path, significance_level: Literal["manual", "required", "optional"], key: str) -> list[RequirementInfo]:
|
||
with open(str(path), 'r') as f:
|
||
res = []
|
||
# Читаем строки, игнорируем пустые и комментарии
|
||
packages = [
|
||
line.strip() for line in f
|
||
if line.strip() and not line.strip().startswith('#')
|
||
]
|
||
for pkg in packages:
|
||
res.append(RequirementInfo.from_requirement_string(pkg, significance_level, key))
|
||
return res
|
||
|
||
@classmethod
|
||
def filter_reqs(cls, req_list: list[RequirementInfo], pinned: str, excluded: str):
|
||
# Загружаем исключения из pinned файла
|
||
if pinned and os.path.exists(pinned):
|
||
pinned_packages = list(cls._load_req_file(pinned, "required", 'pinned'))
|
||
|
||
# Удаляем исключенные пакеты
|
||
criteria = [c.package_name for c in pinned_packages]
|
||
req_list = [r for r in req_list if r.package_name not in criteria]
|
||
|
||
# Загружаем исключения из файла excluded (удаляем точные совпадения)
|
||
if excluded and os.path.exists(excluded):
|
||
excluded_packages = list(cls._load_req_file(excluded, "required", 'excluded'))
|
||
|
||
# Удаляем исключенные пакеты
|
||
criteria = [c.requirement_str for c in excluded_packages]
|
||
req_list = [r for r in req_list if r.requirement_str not in criteria]
|
||
|
||
return req_list
|
||
|
||
@classmethod
|
||
def load_existent_requirements(cls,
|
||
manual_reqs: str,
|
||
reqs_dir: str,
|
||
pinned: str,
|
||
excluded: str
|
||
) -> list[RequirementInfo]:
|
||
"""
|
||
Загружает и обрабатывает требования из файлов.
|
||
|
||
1. Загружает все файлы из self.config.requirements_dir с расширением .req, создает из их строк
|
||
2. Загружает все файлы из self.config.requirements_dir с расширением .opt в списки словаря opt по ключам, соответствующим именам файлов без расширения.
|
||
3. Удаляет из req и opt все пакеты из файла pinned (версия пакета не важна)
|
||
4. Удаляет из req и opt все пакеты из файла excluded (версия пакета должна полностью совпадать. Например если в требованиях есть torch>=2.7.0 удаляем именно такие строки)
|
||
5. Загружает файл manual_req в один из списков словаря с ключом "_manual"
|
||
6. Возвращает значения
|
||
Args:
|
||
manual_reqs: Путь к файлу списка пакетов, установленных вручную
|
||
reqs_dir: Путь к директории со списками пакетов
|
||
pinned: Путь к файлу с исключениями установки
|
||
excluded: Путь к файлу с исключениями проверки
|
||
|
||
Returns:
|
||
Два словаря списков - req и opt
|
||
"""
|
||
|
||
req_list: list[RequirementInfo] = []
|
||
|
||
# Загружаем .req файлы
|
||
for req_file in Path(reqs_dir).glob("*.req"):
|
||
req_list.extend(cls._load_req_file(req_file, "required", req_file.stem))
|
||
|
||
# Загружаем .opt файлы
|
||
for opt_file in Path(reqs_dir).glob("*.opt"):
|
||
req_list.extend(cls._load_req_file(opt_file, "optional", opt_file.stem))
|
||
|
||
req_list = cls.filter_reqs(req_list, pinned, excluded)
|
||
|
||
# Загружаем manual_reqs файл
|
||
if manual_reqs and os.path.exists(manual_reqs):
|
||
req_list.extend(cls._load_req_file(manual_reqs, "manual", "manual"))
|
||
|
||
# Возвращаем результаты
|
||
return req_list
|
||
|
||
|
||
|
||
|
||
"""
|
||
Теперь давай разработаем статический класс 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
|
||
его алгоритм:
|
||
ЭТАП A: Загрузка
|
||
1. Получает на вход список запрашиваемых пакетов в исходном виде, с версиями или без. И флаги required и interactive
|
||
2. Создает два словаря списков req и opt
|
||
3. Загружает файл self.config.manual_requirements в один из списков словаря с ключом "_manual"
|
||
4. Загружает все файлы из self.config.requirements_dir с расширением .req в списки словаря req по ключам, соответствующим именам файлов без расширения.
|
||
5. Повторяет пункт 4 для файлов .opt и словаря opt
|
||
6. Находит во всех списках пакеты из self.config.pinned_packages и удаляет их
|
||
7. Формирует из словарей списков req и opt соответствующие общие списки пакетов.
|
||
|
||
"""
|
||
|
||
|
||
"""
|
||
Напиши статический класс Loader, статически реализующий следующий метод load_requirements
|
||
Аргументы:
|
||
manual_reqs - Путь к файлу списка пакетов, установленных вручную
|
||
reqs_dir - Путь к директории со списками пакетов, необходимых различным модулям программы
|
||
pinned - Путь к файлу с исключениями установки
|
||
excluded - Путь к файлу с исключениями проверки
|
||
Возвращает: dict[str, list[str]], dict[str, list[str]]
|
||
Два словаря списков - req и opt
|
||
Алгоритм работы:
|
||
1. Создает два словаря списков req и opt
|
||
2. Загружает все файлы из self.config.requirements_dir с расширением .req в списки словаря req по ключам, соответствующим именам файлов без расширения.
|
||
3. Загружает все файлы из self.config.requirements_dir с расширением .opt в списки словаря opt по ключам, соответствующим именам файлов без расширения.
|
||
4. Удаляет из req и opt все пакеты из файла pinned (версия пакета не важна)
|
||
4. Удаляет из req и opt все пакеты из файла excluded (версия пакета должна полностью совпадать. Например если в требованиях есть torch>=2.7.0 удаляем именно такие строки)
|
||
5. Загружает файл manual_req в один из списков словаря с ключом "_manual"
|
||
6. Возвращает значения
|
||
""" |