feat: приоритизация инжекторов (Этапы 1-2)
Реализована система приоритизации инжекторов:
Этап 1 - Базовая модель приоритета (float):
- Добавлено поле priority: float в ConversionPoint
- mark_injector(priority=10.5) для установки приоритета
- Интеграция в graph_walker для выбора пути по приоритету
- Aggregate priority для многошаговых путей
Этап 2 - Относительные приоритеты:
- more_than(target) - приоритет выше чем у target
- less_than(target) - приоритет ниже чем у target
- PriorityResolver для разрешения графа зависимостей
- Топологическая сортировка для вычисления приоритетов
- Обнаружение циклов в приоритетах (CircularDependency)
Файлы:
- priority_types.py - классы MoreThan, LessThan, more_than(), less_than()
- priority_resolver.py - PriorityResolver, CycleDetectedError
- test_priority_stage1.py - 21 тест базовых приоритетов
- test_priority_stage2.py - 18 тестов относительных приоритетов
Пример использования:
@repo.mark_injector(priority=10.0)
def int_to_a_v1(i: int) -> A: ...
@repo.mark_injector(priority=more_than(int_to_a_v1))
def int_to_a_v2(i: int) -> A: ...
@repo.mark_injector(priority=less_than(int_to_a_v2))
def int_to_a_v3(i: int) -> A: ...
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
This commit is contained in:
@@ -12,12 +12,24 @@ breakshaft - библиотека для генерации преобразов
|
||||
|
||||
fn = repo.get_conversion((int,), consumer_function)
|
||||
|
||||
Приоритизация инжекторов:
|
||||
from breakshaft import ConvRepo, more_than, less_than
|
||||
|
||||
repo = ConvRepo()
|
||||
|
||||
@repo.mark_injector(priority=10.0) # Абсолютный приоритет
|
||||
def int_to_a_v1(i: int) -> A: ...
|
||||
|
||||
@repo.mark_injector(priority=more_than(int_to_a_v1)) # Относительный приоритет
|
||||
def int_to_a_v2(i: int) -> A: ...
|
||||
|
||||
Исключения:
|
||||
from breakshaft import (
|
||||
BreakshaftError,
|
||||
NoConversionPath,
|
||||
AmbiguousPath,
|
||||
MissingReturnType,
|
||||
CircularDependency, # Для циклов в относительных приоритетах
|
||||
# ... другие исключения
|
||||
)
|
||||
"""
|
||||
@@ -25,6 +37,7 @@ breakshaft - библиотека для генерации преобразов
|
||||
from .convertor import ConvRepo
|
||||
from .graph_walker import GraphWalker
|
||||
from .models import ConversionPoint, Callgraph, CallgraphVariant, TransformationPoint
|
||||
from .priority_types import more_than, less_than, PriorityValue, MoreThan, LessThan
|
||||
from .exceptions import (
|
||||
BreakshaftError,
|
||||
BreakshaftRuntimeError,
|
||||
@@ -61,6 +74,12 @@ __all__ = [
|
||||
"Callgraph",
|
||||
"CallgraphVariant",
|
||||
"TransformationPoint",
|
||||
# Приоритизация
|
||||
"more_than",
|
||||
"less_than",
|
||||
"PriorityValue",
|
||||
"MoreThan",
|
||||
"LessThan",
|
||||
# Исключения
|
||||
"BreakshaftError",
|
||||
"BreakshaftRuntimeError",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import collections.abc
|
||||
from typing import Optional, Callable, Unpack, TypeVarTuple, TypeVar, Awaitable, Any, Sequence, Iterable
|
||||
from typing import Optional, Callable, Unpack, TypeVarTuple, TypeVar, Awaitable, Any, Sequence, Iterable, Union
|
||||
|
||||
from .graph_walker import GraphWalker
|
||||
from .models import ConversionPoint, Callgraph
|
||||
@@ -13,6 +13,8 @@ from .exceptions import (
|
||||
InvalidOptions,
|
||||
MissingDependency,
|
||||
)
|
||||
from .priority_types import PriorityValue, RelativePriority, MoreThan, LessThan
|
||||
from .priority_resolver import resolve_priorities, CycleDetectedError
|
||||
|
||||
Tin = TypeVarTuple('Tin')
|
||||
Tout = TypeVar('Tout')
|
||||
@@ -50,6 +52,9 @@ class ConvRepo:
|
||||
allow_sync: bool = True,
|
||||
force_async: bool = False
|
||||
):
|
||||
# Разрешаем относительные приоритеты
|
||||
self._resolve_relative_priorities()
|
||||
|
||||
filtered_injectors = self.filtered_injectors(allow_async, allow_sync)
|
||||
pipeline_callseq = []
|
||||
orig_from_types = tuple(from_types)
|
||||
@@ -91,8 +96,16 @@ class ConvRepo:
|
||||
def add_injector(self,
|
||||
func: Callable,
|
||||
rettype: Optional[type] = None,
|
||||
type_remap: Optional[dict[str, type]] = None):
|
||||
self.add_conversion_points(ConversionPoint.from_fn(func, rettype=rettype, type_remap=type_remap))
|
||||
type_remap: Optional[dict[str, type]] = None,
|
||||
priority: PriorityValue = 0.0):
|
||||
cps = ConversionPoint.from_fn(func, rettype=rettype, type_remap=type_remap)
|
||||
# Применяем приоритет ко всем ConversionPoint (может быть несколько для Union/tuple)
|
||||
prioritized_cps = [cp.copy_with(priority=priority) for cp in cps]
|
||||
|
||||
# Удаляем существующие инжекторы для этой функции (если есть)
|
||||
self._convertor_set = {cp for cp in self._convertor_set if cp.fn is not func}
|
||||
|
||||
self.add_conversion_points(prioritized_cps)
|
||||
|
||||
def _callseq_from_callgraph(self, cg: Callgraph) -> list[ConversionPoint]:
|
||||
if len(cg.variants) == 0:
|
||||
@@ -209,8 +222,11 @@ class ConvRepo:
|
||||
reason="force_async=True requires allow_async=True"
|
||||
)
|
||||
|
||||
# Разрешаем относительные приоритеты
|
||||
self._resolve_relative_priorities()
|
||||
|
||||
filtered_injectors = self.filtered_injectors(allow_async, allow_sync)
|
||||
|
||||
|
||||
callseq = self.get_callseq(
|
||||
filtered_injectors,
|
||||
frozenset(from_types),
|
||||
@@ -223,13 +239,51 @@ class ConvRepo:
|
||||
setattr(ret_fn, '__breakshaft_callseq__', callseq)
|
||||
return ret_fn
|
||||
|
||||
def mark_injector(self, *, rettype: Optional[type] = None, type_remap: Optional[dict[str, type]] = None):
|
||||
def mark_injector(self, *,
|
||||
rettype: Optional[type] = None,
|
||||
type_remap: Optional[dict[str, type]] = None,
|
||||
priority: PriorityValue = 0.0):
|
||||
def inner(func: Callable):
|
||||
self.add_injector(func, rettype=rettype, type_remap=type_remap)
|
||||
self.add_injector(func, rettype=rettype, type_remap=type_remap, priority=priority)
|
||||
return func
|
||||
|
||||
return inner
|
||||
|
||||
def _resolve_relative_priorities(self):
|
||||
"""
|
||||
Разрешить относительные приоритеты и вычислить абсолютные значения.
|
||||
|
||||
Проходит по всем инжекторам и если есть относительные приоритеты,
|
||||
вычисляет абсолютные значения на основе графа зависимостей.
|
||||
"""
|
||||
injectors = list(self.convertor_set)
|
||||
|
||||
# Проверяем есть ли относительные приоритеты
|
||||
has_relative = any(isinstance(cp.priority, RelativePriority) for cp in injectors)
|
||||
if not has_relative:
|
||||
return
|
||||
|
||||
try:
|
||||
priorities = resolve_priorities(injectors)
|
||||
|
||||
# Применяем разрешённые приоритеты
|
||||
# Создаём новые ConversionPoint с абсолютными приоритетами
|
||||
new_injectors = set()
|
||||
for cp in injectors:
|
||||
if cp in priorities:
|
||||
new_injectors.add(cp.copy_with(priority=priorities[cp]))
|
||||
else:
|
||||
new_injectors.add(cp)
|
||||
|
||||
# Обновляем репозиторий
|
||||
self._convertor_set = new_injectors
|
||||
|
||||
except CycleDetectedError as e:
|
||||
# Переупаковываем в наше исключение
|
||||
from .exceptions import CircularDependency
|
||||
cycle_types = [cp.injects for cp in e.cycle]
|
||||
raise CircularDependency(cycle_types) from e
|
||||
|
||||
def fork(self, fork_with: Optional[set[ConversionPoint]] = None) -> ConvRepo:
|
||||
return ForkedConvRepo(self, fork_with or None,
|
||||
self.walker,
|
||||
|
||||
@@ -138,6 +138,7 @@ class GraphWalker:
|
||||
-> list[CallgraphVariant]:
|
||||
|
||||
if relevance_metric is None:
|
||||
# Сначала применяем стандартные метрики
|
||||
template_metrics = [
|
||||
lambda x: len(x.consumed_from_types),
|
||||
lambda x: x.consumed_cumsum,
|
||||
@@ -151,10 +152,25 @@ class GraphWalker:
|
||||
if len(new_variants) > 0:
|
||||
variants = new_variants
|
||||
|
||||
# Если всё ещё несколько вариантов, используем приоритеты
|
||||
if len(variants) > 1:
|
||||
# sorting by first injector func name for creating minimal cosistancy
|
||||
# could lead to heizenbugs due to incosistancy in path selection between calls
|
||||
variants.sort(key=lambda x: universal_qualname(x.injector.fn))
|
||||
# Вычисляем aggregate priority для каждого варианта (сумма приоритетов всех инжекторов в пути)
|
||||
def get_aggregate_priority(variant: CallgraphVariant) -> float:
|
||||
priority = variant.injector.priority
|
||||
for subg in variant.subgraphs:
|
||||
for subv in subg.variants:
|
||||
priority += get_aggregate_priority(subv)
|
||||
return priority
|
||||
|
||||
# Сортировка по aggregate priority (обратный порядок - выше приоритет = раньше)
|
||||
# Затем по имени функции для детерминизма
|
||||
variants.sort(key=lambda x: (-get_aggregate_priority(x), universal_qualname(x.injector.fn)))
|
||||
|
||||
# Выбираем вариант с наивысшим aggregate приоритетом
|
||||
max_priority = get_aggregate_priority(variants[0])
|
||||
selected = [v for v in variants if get_aggregate_priority(v) == max_priority]
|
||||
variants = selected
|
||||
|
||||
return variants
|
||||
|
||||
if len(variants) < 2:
|
||||
|
||||
@@ -17,11 +17,23 @@ from .exceptions import MissingReturnType, MissingParamType
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ConversionPoint:
|
||||
"""
|
||||
Точка преобразования типов.
|
||||
|
||||
Attributes:
|
||||
fn: Функция-инжектор
|
||||
injects: Тип, который производит инжектор
|
||||
rettype: Фактический тип возврата функции
|
||||
requires: Обязательные типы аргументов
|
||||
opt_args: Опциональные типы аргументов (с default)
|
||||
priority: Приоритет инжектора (float, по умолчанию 0.0)
|
||||
"""
|
||||
fn: Callable
|
||||
injects: type
|
||||
rettype: type
|
||||
requires: tuple[type, ...]
|
||||
opt_args: tuple[type, ...]
|
||||
priority: float = 0.0
|
||||
|
||||
def copy_with(self, **kwargs):
|
||||
fn = kwargs.get('fn', self.fn)
|
||||
@@ -29,7 +41,8 @@ class ConversionPoint:
|
||||
injects = kwargs.get('injects', self.injects)
|
||||
requires = kwargs.get('requires', self.requires)
|
||||
opt_args = kwargs.get('opt_args', self.opt_args)
|
||||
return ConversionPoint(fn, injects, rettype, requires, opt_args)
|
||||
priority = kwargs.get('priority', self.priority)
|
||||
return ConversionPoint(fn, injects, rettype, requires, opt_args, priority)
|
||||
|
||||
def __hash__(self):
|
||||
return hash((self.fn, self.injects, self.requires))
|
||||
|
||||
231
src/breakshaft/priority_resolver.py
Normal file
231
src/breakshaft/priority_resolver.py
Normal file
@@ -0,0 +1,231 @@
|
||||
"""
|
||||
Разрешение относительных приоритетов.
|
||||
|
||||
Модуль для разрешения графа зависимостей относительных приоритетов
|
||||
и вычисления абсолютных значений приоритетов.
|
||||
"""
|
||||
|
||||
from typing import Dict, List, Set, Tuple, Any, Callable
|
||||
from dataclasses import dataclass
|
||||
|
||||
from .models import ConversionPoint
|
||||
from .priority_types import RelativePriority, MoreThan, LessThan, PriorityValue
|
||||
|
||||
|
||||
@dataclass
|
||||
class PriorityConstraint:
|
||||
"""
|
||||
Ограничение приоритета.
|
||||
|
||||
Attributes:
|
||||
from_cp: Инжектор у которого есть ограничение
|
||||
to_cp: Инжектор с которым сравнивается
|
||||
direction: Направление сравнения (+1 для more_than, -1 для less_than)
|
||||
"""
|
||||
from_cp: ConversionPoint
|
||||
to_cp: ConversionPoint
|
||||
direction: int # +1 = from > to, -1 = from < to
|
||||
|
||||
|
||||
class PriorityResolver:
|
||||
"""
|
||||
Разрешатель приоритетов.
|
||||
|
||||
Разрешает граф относительных приоритетов и вычисляет
|
||||
абсолютные значения приоритетов для всех инжекторов.
|
||||
|
||||
Пример использования:
|
||||
resolver = PriorityResolver()
|
||||
resolver.add_constraint(cp1, cp2, direction=1) # cp1 > cp2
|
||||
priorities = resolver.resolve()
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.constraints: List[PriorityConstraint] = []
|
||||
self.injectors: Set[ConversionPoint] = set()
|
||||
|
||||
def add_injector(self, cp: ConversionPoint):
|
||||
"""Добавить инжектор."""
|
||||
self.injectors.add(cp)
|
||||
|
||||
def add_constraint(self, from_cp: ConversionPoint, to_cp: ConversionPoint, direction: int):
|
||||
"""
|
||||
Добавить ограничение приоритета.
|
||||
|
||||
Args:
|
||||
from_cp: Инжектор у которого есть ограничение
|
||||
to_cp: Инжектор с которым сравнивается
|
||||
direction: +1 для from > to, -1 для from < to
|
||||
"""
|
||||
self.constraints.append(PriorityConstraint(from_cp, to_cp, direction))
|
||||
|
||||
def resolve(self) -> Dict[ConversionPoint, float]:
|
||||
"""
|
||||
Разрешить приоритеты и вычислить абсолютные значения.
|
||||
|
||||
Returns:
|
||||
Dict[ConversionPoint, float]: Словарь {инжектор: приоритет}
|
||||
|
||||
Raises:
|
||||
CycleDetectedError: Если обнаружен цикл в ограничениях
|
||||
"""
|
||||
# Построение графа зависимостей
|
||||
# graph[a] = [b, c] означает a > b, a > c
|
||||
graph: Dict[ConversionPoint, List[ConversionPoint]] = {cp: [] for cp in self.injectors}
|
||||
in_degree: Dict[ConversionPoint, int] = {cp: 0 for cp in self.injectors}
|
||||
|
||||
for constraint in self.constraints:
|
||||
if constraint.direction == 1: # from > to
|
||||
graph[constraint.from_cp].append(constraint.to_cp)
|
||||
in_degree[constraint.to_cp] += 1
|
||||
else: # from < to, значит to > from
|
||||
graph[constraint.to_cp].append(constraint.from_cp)
|
||||
in_degree[constraint.from_cp] += 1
|
||||
|
||||
# Топологическая сортировка (алгоритм Кана)
|
||||
queue = [cp for cp in self.injectors if in_degree[cp] == 0]
|
||||
sorted_cps: List[ConversionPoint] = []
|
||||
|
||||
while queue:
|
||||
# Сортируем для детерминизма
|
||||
queue.sort(key=lambda x: id(x))
|
||||
cp = queue.pop(0)
|
||||
sorted_cps.append(cp)
|
||||
|
||||
for neighbor in graph[cp]:
|
||||
in_degree[neighbor] -= 1
|
||||
if in_degree[neighbor] == 0:
|
||||
queue.append(neighbor)
|
||||
|
||||
# Проверка на циклы
|
||||
if len(sorted_cps) != len(self.injectors):
|
||||
# Нашли цикл
|
||||
raise CycleDetectedError(self._find_cycle(graph))
|
||||
|
||||
# Вычисление приоритетов
|
||||
# Инжекторы в начале sorted_cps имеют высший приоритет
|
||||
priorities: Dict[ConversionPoint, float] = {}
|
||||
base_priority = len(sorted_cps) # Начинаем с высокого приоритета
|
||||
|
||||
for i, cp in enumerate(sorted_cps):
|
||||
priorities[cp] = base_priority - i
|
||||
|
||||
return priorities
|
||||
|
||||
def _find_cycle(self, graph: Dict[ConversionPoint, List[ConversionPoint]]) -> List[ConversionPoint]:
|
||||
"""
|
||||
Найти цикл в графе ограничений.
|
||||
|
||||
Returns:
|
||||
List[ConversionPoint]: Цикл (список инжекторов)
|
||||
"""
|
||||
visited: Set[ConversionPoint] = set()
|
||||
rec_stack: Set[ConversionPoint] = set()
|
||||
path: List[ConversionPoint] = []
|
||||
|
||||
def dfs(cp: ConversionPoint) -> bool:
|
||||
visited.add(cp)
|
||||
rec_stack.add(cp)
|
||||
path.append(cp)
|
||||
|
||||
for neighbor in graph[cp]:
|
||||
if neighbor not in visited:
|
||||
if dfs(neighbor):
|
||||
return True
|
||||
elif neighbor in rec_stack:
|
||||
# Нашли цикл
|
||||
cycle_start = path.index(neighbor)
|
||||
return True
|
||||
|
||||
path.pop()
|
||||
rec_stack.remove(cp)
|
||||
return False
|
||||
|
||||
for cp in self.injectors:
|
||||
if cp not in visited:
|
||||
if dfs(cp):
|
||||
# Извлекаем цикл из path
|
||||
cycle_start = len(path) - 1
|
||||
while cycle_start > 0 and path[cycle_start] != path[-1]:
|
||||
cycle_start -= 1
|
||||
return path[cycle_start:] + [path[cycle_start]]
|
||||
|
||||
return []
|
||||
|
||||
|
||||
class CycleDetectedError(Exception):
|
||||
"""
|
||||
Исключение: обнаружен цикл в ограничениях приоритетов.
|
||||
|
||||
Attributes:
|
||||
cycle: Список инжекторов образующих цикл
|
||||
"""
|
||||
|
||||
def __init__(self, cycle: List[ConversionPoint]):
|
||||
self.cycle = cycle
|
||||
cycle_str = " -> ".join(cp.fn.__qualname__ for cp in cycle)
|
||||
super().__init__(f"Priority cycle detected: {cycle_str}")
|
||||
|
||||
|
||||
def resolve_priorities(
|
||||
injectors: List[ConversionPoint]
|
||||
) -> Dict[ConversionPoint, float]:
|
||||
"""
|
||||
Разрешить приоритеты для списка инжекторов.
|
||||
|
||||
Args:
|
||||
injectors: Список инжекторов с относительными приоритетами
|
||||
|
||||
Returns:
|
||||
Dict[ConversionPoint, float]: Словарь {инжектор: абсолютный приоритет}
|
||||
|
||||
Raises:
|
||||
CycleDetectedError: Если обнаружен цикл
|
||||
"""
|
||||
resolver = PriorityResolver()
|
||||
|
||||
# Добавляем все инжекторы
|
||||
for cp in injectors:
|
||||
resolver.add_injector(cp)
|
||||
|
||||
# Добавляем ограничения из относительных приоритетов
|
||||
for cp in injectors:
|
||||
if isinstance(cp.priority, RelativePriority):
|
||||
relative = cp.priority
|
||||
target = _find_target_injector(relative.target, injectors)
|
||||
|
||||
if target is not None:
|
||||
if isinstance(relative, MoreThan):
|
||||
resolver.add_constraint(cp, target, direction=1)
|
||||
elif isinstance(relative, LessThan):
|
||||
resolver.add_constraint(cp, target, direction=-1)
|
||||
|
||||
return resolver.resolve()
|
||||
|
||||
|
||||
def _find_target_injector(
|
||||
target: Any,
|
||||
injectors: List[ConversionPoint]
|
||||
) -> ConversionPoint:
|
||||
"""
|
||||
Найти целевой инжектор по ссылке.
|
||||
|
||||
Args:
|
||||
target: Цель (функция или ConversionPoint)
|
||||
injectors: Список инжекторов для поиска
|
||||
|
||||
Returns:
|
||||
ConversionPoint или None если не найден
|
||||
"""
|
||||
for cp in injectors:
|
||||
if cp.fn is target or cp is target:
|
||||
return cp
|
||||
return None
|
||||
|
||||
|
||||
__all__ = [
|
||||
"PriorityResolver",
|
||||
"PriorityConstraint",
|
||||
"CycleDetectedError",
|
||||
"resolve_priorities",
|
||||
]
|
||||
100
src/breakshaft/priority_types.py
Normal file
100
src/breakshaft/priority_types.py
Normal file
@@ -0,0 +1,100 @@
|
||||
"""
|
||||
Модуль относительных приоритетов для breakshaft.
|
||||
|
||||
Позволяет указывать приоритеты относительно других инжекторов:
|
||||
- more_than(other) - приоритет выше чем у other
|
||||
- less_than(other) - приоритет ниже чем у other
|
||||
|
||||
Пример использования:
|
||||
@repo.mark_injector(priority=more_than(int_to_a_v1))
|
||||
def int_to_a_v2(i: int) -> A: ...
|
||||
|
||||
@repo.mark_injector(priority=less_than(int_to_a_v2))
|
||||
def int_to_a_v3(i: int) -> A: ...
|
||||
"""
|
||||
|
||||
from typing import Callable, Union, Any
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class RelativePriority:
|
||||
"""
|
||||
Базовый класс относительного приоритета.
|
||||
|
||||
Attributes:
|
||||
target: Целевой инжектор (функция или ConversionPoint)
|
||||
"""
|
||||
target: Union[Callable, Any]
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class MoreThan(RelativePriority):
|
||||
"""
|
||||
Приоритет выше чем у целевого инжектора.
|
||||
|
||||
Пример:
|
||||
@repo.mark_injector(priority=more_than(int_to_a_v1))
|
||||
def int_to_a_v2(i: int) -> A: ...
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class LessThan(RelativePriority):
|
||||
"""
|
||||
Приоритет ниже чем у целевого инжектора.
|
||||
|
||||
Пример:
|
||||
@repo.mark_injector(priority=less_than(int_to_a_v2))
|
||||
def int_to_a_v1(i: int) -> A: ...
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
def more_than(target: Union[Callable, Any]) -> MoreThan:
|
||||
"""
|
||||
Создать ограничение "приоритет выше чем у target".
|
||||
|
||||
Args:
|
||||
target: Целевой инжектор (функция или ConversionPoint)
|
||||
|
||||
Returns:
|
||||
MoreThan: Ограничение приоритета
|
||||
|
||||
Пример:
|
||||
>>> @repo.mark_injector(priority=more_than(int_to_a_v1))
|
||||
... def int_to_a_v2(i: int) -> A: ...
|
||||
"""
|
||||
return MoreThan(target)
|
||||
|
||||
|
||||
def less_than(target: Union[Callable, Any]) -> LessThan:
|
||||
"""
|
||||
Создать ограничение "приоритет ниже чем у target".
|
||||
|
||||
Args:
|
||||
target: Целевой инжектор (функция или ConversionPoint)
|
||||
|
||||
Returns:
|
||||
LessThan: Ограничение приоритета
|
||||
|
||||
Пример:
|
||||
>>> @repo.mark_injector(priority=less_than(int_to_a_v2))
|
||||
... def int_to_a_v1(i: int) -> A: ...
|
||||
"""
|
||||
return LessThan(target)
|
||||
|
||||
|
||||
# Тип для приоритетов (абсолютный или относительный)
|
||||
PriorityValue = Union[float, RelativePriority]
|
||||
|
||||
|
||||
__all__ = [
|
||||
"RelativePriority",
|
||||
"MoreThan",
|
||||
"LessThan",
|
||||
"more_than",
|
||||
"less_than",
|
||||
"PriorityValue",
|
||||
]
|
||||
426
tests/test_priority_stage1.py
Normal file
426
tests/test_priority_stage1.py
Normal file
@@ -0,0 +1,426 @@
|
||||
"""
|
||||
Тесты приоритизации инжекторов - Этап 1: Базовая модель приоритета (float).
|
||||
|
||||
Проверка:
|
||||
- Сохранение приоритета в ConversionPoint
|
||||
- Передача приоритета через mark_injector(priority=...)
|
||||
- Выбор пути с наивысшим приоритетом
|
||||
- Детерминизм выбора при одинаковых приоритетах
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
import pytest
|
||||
|
||||
from breakshaft import ConvRepo
|
||||
from breakshaft.models import ConversionPoint
|
||||
|
||||
|
||||
@dataclass
|
||||
class A:
|
||||
a: int
|
||||
|
||||
|
||||
@dataclass
|
||||
class B:
|
||||
b: float
|
||||
|
||||
|
||||
@dataclass
|
||||
class C:
|
||||
c: str
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Юнит-тесты: ConversionPoint с приоритетом
|
||||
# =============================================================================
|
||||
|
||||
class TestConversionPointPriority:
|
||||
"""Тесты хранения приоритета в ConversionPoint."""
|
||||
|
||||
def test_default_priority_is_zero(self):
|
||||
"""Приоритет по умолчанию равен 0.0."""
|
||||
def func(i: int) -> A:
|
||||
return A(i)
|
||||
|
||||
cps = ConversionPoint.from_fn(func)
|
||||
assert len(cps) > 0
|
||||
for cp in cps:
|
||||
assert cp.priority == 0.0
|
||||
|
||||
def test_priority_preserved_in_copy_with(self):
|
||||
"""copy_with сохраняет приоритет."""
|
||||
def func(i: int) -> A:
|
||||
return A(i)
|
||||
|
||||
cps = ConversionPoint.from_fn(func)
|
||||
cp = cps[0]
|
||||
|
||||
# Создаём копию с изменённым injects
|
||||
cp_copy = cp.copy_with(injects=B)
|
||||
|
||||
# Приоритет должен сохраниться
|
||||
assert cp_copy.priority == cp.priority
|
||||
|
||||
def test_priority_can_be_set_via_copy_with(self):
|
||||
"""copy_with может изменять приоритет."""
|
||||
def func(i: int) -> A:
|
||||
return A(i)
|
||||
|
||||
cps = ConversionPoint.from_fn(func)
|
||||
cp = cps[0]
|
||||
|
||||
# Изменяем приоритет
|
||||
cp_copy = cp.copy_with(priority=10.5)
|
||||
|
||||
assert cp_copy.priority == 10.5
|
||||
assert cp.priority == 0.0 # Оригинал не изменился
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Юнит-тесты: mark_injector с приоритетом
|
||||
# =============================================================================
|
||||
|
||||
class TestMarkInjectorPriority:
|
||||
"""Тесты декоратора mark_injector с приоритетом."""
|
||||
|
||||
def test_mark_injector_default_priority(self):
|
||||
"""mark_injector без priority использует 0.0."""
|
||||
repo = ConvRepo()
|
||||
|
||||
@repo.mark_injector()
|
||||
def int_to_a(i: int) -> A:
|
||||
return A(i)
|
||||
|
||||
cps = list(repo.convertor_set)
|
||||
assert len(cps) == 1
|
||||
assert cps[0].priority == 0.0
|
||||
|
||||
def test_mark_injector_with_priority(self):
|
||||
"""mark_injector(priority=X) устанавливает приоритет."""
|
||||
repo = ConvRepo()
|
||||
|
||||
@repo.mark_injector(priority=10.5)
|
||||
def int_to_a(i: int) -> A:
|
||||
return A(i)
|
||||
|
||||
cps = list(repo.convertor_set)
|
||||
assert len(cps) == 1
|
||||
assert cps[0].priority == 10.5
|
||||
|
||||
def test_mark_injector_negative_priority(self):
|
||||
"""mark_injector поддерживает отрицательные приоритеты."""
|
||||
repo = ConvRepo()
|
||||
|
||||
@repo.mark_injector(priority=-5.0)
|
||||
def int_to_a(i: int) -> A:
|
||||
return A(i)
|
||||
|
||||
cps = list(repo.convertor_set)
|
||||
assert len(cps) == 1
|
||||
assert cps[0].priority == -5.0
|
||||
|
||||
def test_mark_injector_priority_with_rettype(self):
|
||||
"""mark_injector(priority=..., rettype=...) работает корректно."""
|
||||
repo = ConvRepo()
|
||||
|
||||
@repo.mark_injector(priority=7.5, rettype=A)
|
||||
def int_to_a(i: int):
|
||||
return A(i)
|
||||
|
||||
cps = list(repo.convertor_set)
|
||||
assert len(cps) == 1
|
||||
assert cps[0].priority == 7.5
|
||||
assert cps[0].injects == A
|
||||
|
||||
def test_mark_injector_priority_with_type_remap(self):
|
||||
"""mark_injector(priority=..., type_remap=...) работает корректно."""
|
||||
repo = ConvRepo()
|
||||
|
||||
type NewA = A
|
||||
type_remap = {'i': int, 'return': NewA}
|
||||
|
||||
@repo.mark_injector(priority=3.0, type_remap=type_remap)
|
||||
def int_to_a(i: int) -> NewA:
|
||||
return NewA(i)
|
||||
|
||||
cps = list(repo.convertor_set)
|
||||
assert len(cps) == 1
|
||||
assert cps[0].priority == 3.0
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Интеграционные тесты: Выбор пути по приоритету
|
||||
# =============================================================================
|
||||
|
||||
class TestPriorityPathSelection:
|
||||
"""Тесты выбора пути преобразования по приоритету."""
|
||||
|
||||
def test_higher_priority_path_selected(self):
|
||||
"""Путь с более высоким приоритетом выбирается."""
|
||||
repo = ConvRepo()
|
||||
|
||||
@repo.mark_injector(priority=1.0)
|
||||
def int_to_a_low(i: int) -> A:
|
||||
return A(i * 10) # Низкий приоритет: A(420)
|
||||
|
||||
@repo.mark_injector(priority=10.0)
|
||||
def int_to_a_high(i: int) -> A:
|
||||
return A(i + 100) # Высокий приоритет: A(142)
|
||||
|
||||
def consumer(dep: A) -> int:
|
||||
return dep.a
|
||||
|
||||
# force_commutative=False позволяет выбрать любой путь
|
||||
fn = repo.get_conversion((int,), consumer, force_commutative=False)
|
||||
result = fn(42)
|
||||
|
||||
# Должен выбрать путь с высоким приоритетом (42 + 100 = 142)
|
||||
assert result == 142
|
||||
|
||||
def test_lower_priority_path_selected_when_higher_not_available(self):
|
||||
"""Путь с низким приоритетом выбирается если высокий недоступен."""
|
||||
repo = ConvRepo()
|
||||
|
||||
@repo.mark_injector(priority=1.0)
|
||||
def int_to_a(i: int) -> A:
|
||||
return A(i * 10)
|
||||
|
||||
def consumer(dep: A) -> int:
|
||||
return dep.a
|
||||
|
||||
fn = repo.get_conversion((int,), consumer)
|
||||
result = fn(42)
|
||||
|
||||
assert result == 420
|
||||
|
||||
def test_equal_priorities_use_fallback(self):
|
||||
"""При одинаковых приоритетах используется fallback (имя функции)."""
|
||||
repo = ConvRepo()
|
||||
|
||||
@repo.mark_injector(priority=5.0)
|
||||
def aaa_converter(i: int) -> A:
|
||||
return A(i * 10)
|
||||
|
||||
@repo.mark_injector(priority=5.0)
|
||||
def zzz_converter(i: int) -> A:
|
||||
return A(i + 100)
|
||||
|
||||
def consumer(dep: A) -> int:
|
||||
return dep.a
|
||||
|
||||
# При одинаковых приоритетах выбор детерминирован (по имени функции)
|
||||
fn = repo.get_conversion((int,), consumer, force_commutative=False)
|
||||
result = fn(42)
|
||||
|
||||
# Должен выбрать один из путей (детерминировано)
|
||||
assert result in [420, 142]
|
||||
|
||||
def test_priority_with_multiple_steps(self):
|
||||
"""Приоритеты работают в многошаговых преобразованиях."""
|
||||
repo = ConvRepo()
|
||||
|
||||
@repo.mark_injector(priority=1.0)
|
||||
def int_to_b_low(i: int) -> B:
|
||||
return B(float(i) * 10)
|
||||
|
||||
@repo.mark_injector(priority=10.0)
|
||||
def int_to_b_high(i: int) -> B:
|
||||
return B(float(i) + 100)
|
||||
|
||||
@repo.mark_injector()
|
||||
def b_to_a(b: B) -> A:
|
||||
return A(int(b.b))
|
||||
|
||||
def consumer(dep: B) -> float:
|
||||
return dep.b
|
||||
|
||||
# Тестируем выбор int->B (первый шаг)
|
||||
fn = repo.get_conversion((int,), consumer, force_commutative=False)
|
||||
result = fn(42)
|
||||
|
||||
# Должен выбрать int->B(high): (42 + 100) = 142
|
||||
assert result == 142.0
|
||||
|
||||
def test_priority_deterministic_selection(self):
|
||||
"""Приоритеты обеспечивают детерминированный выбор."""
|
||||
repo = ConvRepo()
|
||||
|
||||
@repo.mark_injector(priority=1.0)
|
||||
def int_to_a_v1(i: int) -> A:
|
||||
return A(i * 10)
|
||||
|
||||
@repo.mark_injector(priority=10.0)
|
||||
def int_to_a_v2(i: int) -> A:
|
||||
return A(i + 100)
|
||||
|
||||
def consumer(dep: A) -> int:
|
||||
return dep.a
|
||||
|
||||
# Запускаем много раз - результат должен быть одинаковым
|
||||
fn = repo.get_conversion((int,), consumer, force_commutative=False)
|
||||
results = [fn(42) for _ in range(10)]
|
||||
|
||||
# Все результаты должны быть одинаковыми (детерминизм)
|
||||
assert len(set(results)) == 1
|
||||
assert results[0] == 142 # Высокий приоритет
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Интеграционные тесты: add_injector с приоритетом
|
||||
# =============================================================================
|
||||
|
||||
class TestAddInjectorPriority:
|
||||
"""Тесты функции add_injector с приоритетом."""
|
||||
|
||||
def test_add_injector_with_priority(self):
|
||||
"""add_injector(priority=...) устанавливает приоритет."""
|
||||
repo = ConvRepo()
|
||||
|
||||
def int_to_a(i: int) -> A:
|
||||
return A(i)
|
||||
|
||||
repo.add_injector(int_to_a, priority=5.5)
|
||||
|
||||
cps = list(repo.convertor_set)
|
||||
assert len(cps) == 1
|
||||
assert cps[0].priority == 5.5
|
||||
|
||||
def test_add_injector_default_priority(self):
|
||||
"""add_injector без priority использует 0.0."""
|
||||
repo = ConvRepo()
|
||||
|
||||
def int_to_a(i: int) -> A:
|
||||
return A(i)
|
||||
|
||||
repo.add_injector(int_to_a)
|
||||
|
||||
cps = list(repo.convertor_set)
|
||||
assert len(cps) == 1
|
||||
assert cps[0].priority == 0.0
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Тесты: Приоритеты с Union-типами
|
||||
# =============================================================================
|
||||
|
||||
class TestPriorityWithUnionTypes:
|
||||
"""Тесты приоритетов с Union-типами."""
|
||||
|
||||
def test_priority_with_union_return_type(self):
|
||||
"""Приоритет применяется ко всем вариантам Union return type."""
|
||||
repo = ConvRepo()
|
||||
|
||||
@repo.mark_injector(priority=7.0)
|
||||
def int_to_a_or_b(i: int) -> A | B:
|
||||
return A(i)
|
||||
|
||||
cps = list(repo.convertor_set)
|
||||
# Для Union создаётся несколько ConversionPoint
|
||||
assert len(cps) > 0
|
||||
# Все должны иметь одинаковый приоритет
|
||||
for cp in cps:
|
||||
assert cp.priority == 7.0
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Тесты: Приоритеты в конвейерах (pipelines)
|
||||
# =============================================================================
|
||||
|
||||
class TestPriorityInPipelines:
|
||||
"""Тесты приоритетов в конвейерах преобразований."""
|
||||
|
||||
def test_pipeline_respects_priorities(self):
|
||||
"""Конвейер уважает приоритеты инжекторов."""
|
||||
repo = ConvRepo()
|
||||
|
||||
@repo.mark_injector(priority=1.0)
|
||||
def int_to_a_low(i: int) -> A:
|
||||
return A(i * 10)
|
||||
|
||||
@repo.mark_injector(priority=10.0)
|
||||
def int_to_a_high(i: int) -> A:
|
||||
return A(i + 100)
|
||||
|
||||
@repo.mark_injector()
|
||||
def a_to_b(a: A) -> B:
|
||||
return B(float(a.a))
|
||||
|
||||
def consumer1(dep: A) -> B: # Потребляет A напрямую
|
||||
return a_to_b(dep)
|
||||
|
||||
def consumer2(dep: B) -> float:
|
||||
return dep.b
|
||||
|
||||
pipeline = repo.create_pipeline(
|
||||
(int,),
|
||||
[consumer1, consumer2],
|
||||
force_commutative=False
|
||||
)
|
||||
|
||||
result = pipeline(42)
|
||||
|
||||
# Должен выбрать путь с высоким приоритетом: (42 + 100) = 142
|
||||
assert result == 142.0
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Тесты: Краевые случаи
|
||||
# =============================================================================
|
||||
|
||||
class TestPriorityEdgeCases:
|
||||
"""Тесты краевых случаев приоритетов."""
|
||||
|
||||
def test_very_large_priority(self):
|
||||
"""Очень большой приоритет работает корректно."""
|
||||
repo = ConvRepo()
|
||||
|
||||
@repo.mark_injector(priority=1e10)
|
||||
def int_to_a(i: int) -> A:
|
||||
return A(i)
|
||||
|
||||
cps = list(repo.convertor_set)
|
||||
assert len(cps) == 1
|
||||
assert cps[0].priority == 1e10
|
||||
|
||||
def test_very_small_negative_priority(self):
|
||||
"""Очень маленький отрицательный приоритет работает корректно."""
|
||||
repo = ConvRepo()
|
||||
|
||||
@repo.mark_injector(priority=-1e10)
|
||||
def int_to_a(i: int) -> A:
|
||||
return A(i)
|
||||
|
||||
cps = list(repo.convertor_set)
|
||||
assert len(cps) == 1
|
||||
assert cps[0].priority == -1e10
|
||||
|
||||
def test_float_priority_precision(self):
|
||||
"""Дробные приоритеты сохраняют точность."""
|
||||
repo = ConvRepo()
|
||||
|
||||
@repo.mark_injector(priority=3.14159)
|
||||
def int_to_a(i: int) -> A:
|
||||
return A(i)
|
||||
|
||||
cps = list(repo.convertor_set)
|
||||
assert len(cps) == 1
|
||||
assert abs(cps[0].priority - 3.14159) < 1e-10
|
||||
|
||||
def test_zero_priority_same_as_default(self):
|
||||
"""Приоритет 0.0 эквивалентен приориту по умолчанию."""
|
||||
repo1 = ConvRepo()
|
||||
repo2 = ConvRepo()
|
||||
|
||||
@repo1.mark_injector()
|
||||
def int_to_a1(i: int) -> A:
|
||||
return A(i)
|
||||
|
||||
@repo2.mark_injector(priority=0.0)
|
||||
def int_to_a2(i: int) -> A:
|
||||
return A(i)
|
||||
|
||||
cps1 = list(repo1.convertor_set)
|
||||
cps2 = list(repo2.convertor_set)
|
||||
|
||||
assert cps1[0].priority == cps2[0].priority
|
||||
447
tests/test_priority_stage2.py
Normal file
447
tests/test_priority_stage2.py
Normal file
@@ -0,0 +1,447 @@
|
||||
"""
|
||||
Тесты приоритизации инжекторов - Этап 2: Относительные приоритеты.
|
||||
|
||||
Проверка:
|
||||
- more_than() создаёт ограничение приоритета
|
||||
- less_than() создаёт ограничение приоритета
|
||||
- Разрешение графа относительных приоритетов
|
||||
- Обнаружение циклов в приоритетах
|
||||
- Транзитивность приоритетов (A > B > C ⇒ A > C)
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
import pytest
|
||||
|
||||
from breakshaft import ConvRepo, more_than, less_than, CircularDependency
|
||||
from breakshaft.priority_types import MoreThan, LessThan
|
||||
from breakshaft.priority_resolver import PriorityResolver, CycleDetectedError
|
||||
from breakshaft.models import ConversionPoint
|
||||
|
||||
|
||||
@dataclass
|
||||
class A:
|
||||
a: int
|
||||
|
||||
|
||||
@dataclass
|
||||
class B:
|
||||
b: float
|
||||
|
||||
|
||||
@dataclass
|
||||
class C:
|
||||
c: str
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Юнит-тесты: RelativePriority классы
|
||||
# =============================================================================
|
||||
|
||||
class TestRelativePriorityClasses:
|
||||
"""Тесты классов относительных приоритетов."""
|
||||
|
||||
def test_more_than_creation(self):
|
||||
"""more_than() создаёт MoreThan объект."""
|
||||
def func(i: int) -> A:
|
||||
return A(i)
|
||||
|
||||
rel = more_than(func)
|
||||
|
||||
assert isinstance(rel, MoreThan)
|
||||
assert rel.target is func
|
||||
|
||||
def test_less_than_creation(self):
|
||||
"""less_than() создаёт LessThan объект."""
|
||||
def func(i: int) -> A:
|
||||
return A(i)
|
||||
|
||||
rel = less_than(func)
|
||||
|
||||
assert isinstance(rel, LessThan)
|
||||
assert rel.target is func
|
||||
|
||||
def test_more_than_frozen(self):
|
||||
"""MoreThan неизменяемый (frozen dataclass)."""
|
||||
def func(i: int) -> A:
|
||||
return A(i)
|
||||
|
||||
rel = more_than(func)
|
||||
|
||||
with pytest.raises(AttributeError):
|
||||
rel.target = None # type: ignore
|
||||
|
||||
def test_less_than_frozen(self):
|
||||
"""LessThan неизменяемый (frozen dataclass)."""
|
||||
def func(i: int) -> A:
|
||||
return A(i)
|
||||
|
||||
rel = less_than(func)
|
||||
|
||||
with pytest.raises(AttributeError):
|
||||
rel.target = None # type: ignore
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Юнит-тесты: PriorityResolver
|
||||
# =============================================================================
|
||||
|
||||
class TestPriorityResolver:
|
||||
"""Тесты разрешителя приоритетов."""
|
||||
|
||||
def test_simple_more_than(self):
|
||||
"""Простое more_than ограничение."""
|
||||
def func1(i: int) -> A:
|
||||
return A(i)
|
||||
|
||||
def func2(i: int) -> A:
|
||||
return A(i * 10)
|
||||
|
||||
cp1 = ConversionPoint.from_fn(func1)[0]
|
||||
cp2 = ConversionPoint.from_fn(func2)[0]
|
||||
|
||||
cp1 = cp1.copy_with(priority=more_than(func2))
|
||||
|
||||
resolver = PriorityResolver()
|
||||
resolver.add_injector(cp1)
|
||||
resolver.add_injector(cp2)
|
||||
resolver.add_constraint(cp1, cp2, direction=1)
|
||||
|
||||
priorities = resolver.resolve()
|
||||
|
||||
assert priorities[cp1] > priorities[cp2]
|
||||
|
||||
def test_simple_less_than(self):
|
||||
"""Простое less_than ограничение."""
|
||||
def func1(i: int) -> A:
|
||||
return A(i)
|
||||
|
||||
def func2(i: int) -> A:
|
||||
return A(i * 10)
|
||||
|
||||
cp1 = ConversionPoint.from_fn(func1)[0]
|
||||
cp2 = ConversionPoint.from_fn(func2)[0]
|
||||
|
||||
cp1 = cp1.copy_with(priority=less_than(func2))
|
||||
|
||||
resolver = PriorityResolver()
|
||||
resolver.add_injector(cp1)
|
||||
resolver.add_injector(cp2)
|
||||
resolver.add_constraint(cp1, cp2, direction=-1)
|
||||
|
||||
priorities = resolver.resolve()
|
||||
|
||||
assert priorities[cp1] < priorities[cp2]
|
||||
|
||||
def test_transitive_priorities(self):
|
||||
"""Транзитивность приоритетов: A > B > C ⇒ A > C."""
|
||||
def func_a(i: int) -> A:
|
||||
return A(i)
|
||||
|
||||
def func_b(i: int) -> A:
|
||||
return A(i * 10)
|
||||
|
||||
def func_c(i: int) -> A:
|
||||
return A(i * 100)
|
||||
|
||||
cp_a = ConversionPoint.from_fn(func_a)[0]
|
||||
cp_b = ConversionPoint.from_fn(func_b)[0]
|
||||
cp_c = ConversionPoint.from_fn(func_c)[0]
|
||||
|
||||
resolver = PriorityResolver()
|
||||
resolver.add_injector(cp_a)
|
||||
resolver.add_injector(cp_b)
|
||||
resolver.add_injector(cp_c)
|
||||
|
||||
# A > B, B > C
|
||||
resolver.add_constraint(cp_a, cp_b, direction=1)
|
||||
resolver.add_constraint(cp_b, cp_c, direction=1)
|
||||
|
||||
priorities = resolver.resolve()
|
||||
|
||||
assert priorities[cp_a] > priorities[cp_b]
|
||||
assert priorities[cp_b] > priorities[cp_c]
|
||||
assert priorities[cp_a] > priorities[cp_c] # Транзитивность
|
||||
|
||||
def test_cycle_detection(self):
|
||||
"""Обнаружение цикла: A > B > C > A."""
|
||||
def func_a(i: int) -> A:
|
||||
return A(i)
|
||||
|
||||
def func_b(i: int) -> A:
|
||||
return A(i * 10)
|
||||
|
||||
def func_c(i: int) -> A:
|
||||
return A(i * 100)
|
||||
|
||||
cp_a = ConversionPoint.from_fn(func_a)[0]
|
||||
cp_b = ConversionPoint.from_fn(func_b)[0]
|
||||
cp_c = ConversionPoint.from_fn(func_c)[0]
|
||||
|
||||
resolver = PriorityResolver()
|
||||
resolver.add_injector(cp_a)
|
||||
resolver.add_injector(cp_b)
|
||||
resolver.add_injector(cp_c)
|
||||
|
||||
# A > B, B > C, C > A (цикл!)
|
||||
resolver.add_constraint(cp_a, cp_b, direction=1)
|
||||
resolver.add_constraint(cp_b, cp_c, direction=1)
|
||||
resolver.add_constraint(cp_c, cp_a, direction=1)
|
||||
|
||||
with pytest.raises(CycleDetectedError):
|
||||
resolver.resolve()
|
||||
|
||||
def test_multiple_constraints_same_injector(self):
|
||||
"""Несколько ограничений для одного инжектора."""
|
||||
def func_a(i: int) -> A:
|
||||
return A(i)
|
||||
|
||||
def func_b(i: int) -> A:
|
||||
return A(i * 10)
|
||||
|
||||
def func_c(i: int) -> A:
|
||||
return A(i * 100)
|
||||
|
||||
cp_a = ConversionPoint.from_fn(func_a)[0]
|
||||
cp_b = ConversionPoint.from_fn(func_b)[0]
|
||||
cp_c = ConversionPoint.from_fn(func_c)[0]
|
||||
|
||||
resolver = PriorityResolver()
|
||||
resolver.add_injector(cp_a)
|
||||
resolver.add_injector(cp_b)
|
||||
resolver.add_injector(cp_c)
|
||||
|
||||
# A > B, A > C
|
||||
resolver.add_constraint(cp_a, cp_b, direction=1)
|
||||
resolver.add_constraint(cp_a, cp_c, direction=1)
|
||||
|
||||
priorities = resolver.resolve()
|
||||
|
||||
assert priorities[cp_a] > priorities[cp_b]
|
||||
assert priorities[cp_a] > priorities[cp_c]
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Интеграционные тесты: Относительные приоритеты в ConvRepo
|
||||
# =============================================================================
|
||||
|
||||
class TestRelativePrioritiesInRepo:
|
||||
"""Тесты относительных приоритетов в репозитории."""
|
||||
|
||||
def test_more_than_in_mark_injector(self):
|
||||
"""more_than в mark_injector."""
|
||||
repo = ConvRepo()
|
||||
|
||||
@repo.mark_injector()
|
||||
def int_to_a_base(i: int) -> A:
|
||||
return A(i * 10)
|
||||
|
||||
@repo.mark_injector(priority=more_than(int_to_a_base))
|
||||
def int_to_a_preferred(i: int) -> A:
|
||||
return A(i + 100)
|
||||
|
||||
def consumer(dep: A) -> int:
|
||||
return dep.a
|
||||
|
||||
fn = repo.get_conversion((int,), consumer, force_commutative=False)
|
||||
result = fn(42)
|
||||
|
||||
# Должен выбрать путь с высоким приоритетом (42 + 100 = 142)
|
||||
assert result == 142
|
||||
|
||||
def test_less_than_in_mark_injector(self):
|
||||
"""less_than в mark_injector."""
|
||||
repo = ConvRepo()
|
||||
|
||||
# Сначала определяем функцию которая будет "выше"
|
||||
@repo.mark_injector(priority=10.0)
|
||||
def int_to_a_preferred(i: int) -> A:
|
||||
return A(i + 100)
|
||||
|
||||
# Потом функцию с less_than
|
||||
@repo.mark_injector(priority=less_than(int_to_a_preferred))
|
||||
def int_to_a_low(i: int) -> A:
|
||||
return A(i * 10)
|
||||
|
||||
def consumer(dep: A) -> int:
|
||||
return dep.a
|
||||
|
||||
fn = repo.get_conversion((int,), consumer, force_commutative=False)
|
||||
result = fn(42)
|
||||
|
||||
# Должен выбрать путь с высоким приоритетом (42 + 100 = 142)
|
||||
assert result == 142
|
||||
|
||||
def test_chain_more_than(self):
|
||||
"""Цепочка more_than: A > B > C."""
|
||||
repo = ConvRepo()
|
||||
|
||||
@repo.mark_injector()
|
||||
def int_to_a_c(i: int) -> A:
|
||||
return A(i * 100) # Низкий приоритет
|
||||
|
||||
@repo.mark_injector(priority=more_than(int_to_a_c))
|
||||
def int_to_a_b(i: int) -> A:
|
||||
return A(i * 10) # Средний приоритет
|
||||
|
||||
@repo.mark_injector(priority=more_than(int_to_a_b))
|
||||
def int_to_a_a(i: int) -> A:
|
||||
return A(i + 100) # Высокий приоритет
|
||||
|
||||
def consumer(dep: A) -> int:
|
||||
return dep.a
|
||||
|
||||
fn = repo.get_conversion((int,), consumer, force_commutative=False)
|
||||
result = fn(42)
|
||||
|
||||
# Должен выбрать путь с высоким приоритетом (42 + 100 = 142)
|
||||
assert result == 142
|
||||
|
||||
def test_circular_dependency_raises(self):
|
||||
"""Циклическая зависимость вызывает CircularDependency."""
|
||||
repo = ConvRepo()
|
||||
|
||||
# Определяем функции сначала
|
||||
@repo.mark_injector()
|
||||
def int_to_a_a(i: int) -> A:
|
||||
return A(i)
|
||||
|
||||
@repo.mark_injector()
|
||||
def int_to_a_b(i: int) -> A:
|
||||
return A(i * 10)
|
||||
|
||||
@repo.mark_injector()
|
||||
def int_to_a_c(i: int) -> A:
|
||||
return A(i * 100)
|
||||
|
||||
# Теперь добавляем циклические зависимости
|
||||
repo.add_injector(int_to_a_a, priority=more_than(int_to_a_b))
|
||||
repo.add_injector(int_to_a_b, priority=more_than(int_to_a_c))
|
||||
repo.add_injector(int_to_a_c, priority=more_than(int_to_a_a))
|
||||
|
||||
def consumer(dep: A) -> int:
|
||||
return dep.a
|
||||
|
||||
# Цикл: A > B > C > A
|
||||
with pytest.raises(CircularDependency):
|
||||
repo.get_conversion((int,), consumer, force_commutative=False)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Тесты: Смешанные абсолютные и относительные приоритеты
|
||||
# =============================================================================
|
||||
|
||||
class TestMixedPriorities:
|
||||
"""Тесты смешанных абсолютных и относительных приоритетов."""
|
||||
|
||||
def test_absolute_and_relative(self):
|
||||
"""Смешение абсолютных и относительных приоритетов."""
|
||||
repo = ConvRepo()
|
||||
|
||||
@repo.mark_injector(priority=5.0)
|
||||
def int_to_a_base(i: int) -> A:
|
||||
return A(i * 10)
|
||||
|
||||
@repo.mark_injector(priority=more_than(int_to_a_base))
|
||||
def int_to_a_high(i: int) -> A:
|
||||
return A(i + 100)
|
||||
|
||||
def consumer(dep: A) -> int:
|
||||
return dep.a
|
||||
|
||||
fn = repo.get_conversion((int,), consumer, force_commutative=False)
|
||||
result = fn(42)
|
||||
|
||||
# more_than должен дать приоритет выше чем 5.0
|
||||
assert result == 142
|
||||
|
||||
def test_relative_with_absolute_fallback(self):
|
||||
"""Относительный приоритет с абсолютным fallback."""
|
||||
repo = ConvRepo()
|
||||
|
||||
@repo.mark_injector(priority=10.0)
|
||||
def int_to_a_high(i: int) -> A:
|
||||
return A(i + 100)
|
||||
|
||||
@repo.mark_injector(priority=less_than(int_to_a_high))
|
||||
def int_to_a_low(i: int) -> A:
|
||||
return A(i * 10)
|
||||
|
||||
def consumer(dep: A) -> int:
|
||||
return dep.a
|
||||
|
||||
fn = repo.get_conversion((int,), consumer, force_commutative=False)
|
||||
result = fn(42)
|
||||
|
||||
# Должен выбрать путь с высоким приоритетом
|
||||
assert result == 142
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Тесты: Краевые случаи
|
||||
# =============================================================================
|
||||
|
||||
class TestRelativePriorityEdgeCases:
|
||||
"""Тесты краевых случаев относительных приоритетов."""
|
||||
|
||||
def test_self_reference_raises(self):
|
||||
"""Ссылка на себя вызывает цикл."""
|
||||
repo = ConvRepo()
|
||||
|
||||
# Определяем функцию сначала
|
||||
@repo.mark_injector()
|
||||
def int_to_a_self(i: int) -> A:
|
||||
return A(i)
|
||||
|
||||
# Теперь добавляем self-reference
|
||||
repo.add_injector(int_to_a_self, priority=more_than(int_to_a_self))
|
||||
|
||||
def consumer(dep: A) -> int:
|
||||
return dep.a
|
||||
|
||||
with pytest.raises((CircularDependency, CycleDetectedError)):
|
||||
repo.get_conversion((int,), consumer, force_commutative=False)
|
||||
|
||||
def test_non_existent_target_ignored(self):
|
||||
"""Несуществующая цель игнорируется."""
|
||||
repo = ConvRepo()
|
||||
|
||||
def non_existent(i: int) -> A:
|
||||
return A(i)
|
||||
|
||||
@repo.mark_injector(priority=more_than(non_existent))
|
||||
def int_to_a(i: int) -> A:
|
||||
return A(i)
|
||||
|
||||
def consumer(dep: A) -> int:
|
||||
return dep.a
|
||||
|
||||
# Должно работать, non_existent не зарегистрирован
|
||||
fn = repo.get_conversion((int,), consumer, force_commutative=False)
|
||||
result = fn(42)
|
||||
assert result == 42
|
||||
|
||||
def test_multiple_more_than_same_target(self):
|
||||
"""Несколько more_than на одну цель."""
|
||||
repo = ConvRepo()
|
||||
|
||||
@repo.mark_injector()
|
||||
def int_to_a_base(i: int) -> A:
|
||||
return A(i * 10)
|
||||
|
||||
@repo.mark_injector(priority=more_than(int_to_a_base))
|
||||
def int_to_a_v1(i: int) -> A:
|
||||
return A(i + 100)
|
||||
|
||||
@repo.mark_injector(priority=more_than(int_to_a_base))
|
||||
def int_to_a_v2(i: int) -> A:
|
||||
return A(i + 200)
|
||||
|
||||
def consumer(dep: A) -> int:
|
||||
return dep.a
|
||||
|
||||
fn = repo.get_conversion((int,), consumer, force_commutative=False)
|
||||
result = fn(42)
|
||||
|
||||
# Оба v1 и v2 имеют приоритет выше base, выбирается один из них
|
||||
assert result in [142, 242]
|
||||
Reference in New Issue
Block a user