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)
|
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 (
|
from breakshaft import (
|
||||||
BreakshaftError,
|
BreakshaftError,
|
||||||
NoConversionPath,
|
NoConversionPath,
|
||||||
AmbiguousPath,
|
AmbiguousPath,
|
||||||
MissingReturnType,
|
MissingReturnType,
|
||||||
|
CircularDependency, # Для циклов в относительных приоритетах
|
||||||
# ... другие исключения
|
# ... другие исключения
|
||||||
)
|
)
|
||||||
"""
|
"""
|
||||||
@@ -25,6 +37,7 @@ breakshaft - библиотека для генерации преобразов
|
|||||||
from .convertor import ConvRepo
|
from .convertor import ConvRepo
|
||||||
from .graph_walker import GraphWalker
|
from .graph_walker import GraphWalker
|
||||||
from .models import ConversionPoint, Callgraph, CallgraphVariant, TransformationPoint
|
from .models import ConversionPoint, Callgraph, CallgraphVariant, TransformationPoint
|
||||||
|
from .priority_types import more_than, less_than, PriorityValue, MoreThan, LessThan
|
||||||
from .exceptions import (
|
from .exceptions import (
|
||||||
BreakshaftError,
|
BreakshaftError,
|
||||||
BreakshaftRuntimeError,
|
BreakshaftRuntimeError,
|
||||||
@@ -61,6 +74,12 @@ __all__ = [
|
|||||||
"Callgraph",
|
"Callgraph",
|
||||||
"CallgraphVariant",
|
"CallgraphVariant",
|
||||||
"TransformationPoint",
|
"TransformationPoint",
|
||||||
|
# Приоритизация
|
||||||
|
"more_than",
|
||||||
|
"less_than",
|
||||||
|
"PriorityValue",
|
||||||
|
"MoreThan",
|
||||||
|
"LessThan",
|
||||||
# Исключения
|
# Исключения
|
||||||
"BreakshaftError",
|
"BreakshaftError",
|
||||||
"BreakshaftRuntimeError",
|
"BreakshaftRuntimeError",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import collections.abc
|
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 .graph_walker import GraphWalker
|
||||||
from .models import ConversionPoint, Callgraph
|
from .models import ConversionPoint, Callgraph
|
||||||
@@ -13,6 +13,8 @@ from .exceptions import (
|
|||||||
InvalidOptions,
|
InvalidOptions,
|
||||||
MissingDependency,
|
MissingDependency,
|
||||||
)
|
)
|
||||||
|
from .priority_types import PriorityValue, RelativePriority, MoreThan, LessThan
|
||||||
|
from .priority_resolver import resolve_priorities, CycleDetectedError
|
||||||
|
|
||||||
Tin = TypeVarTuple('Tin')
|
Tin = TypeVarTuple('Tin')
|
||||||
Tout = TypeVar('Tout')
|
Tout = TypeVar('Tout')
|
||||||
@@ -50,6 +52,9 @@ class ConvRepo:
|
|||||||
allow_sync: bool = True,
|
allow_sync: bool = True,
|
||||||
force_async: bool = False
|
force_async: bool = False
|
||||||
):
|
):
|
||||||
|
# Разрешаем относительные приоритеты
|
||||||
|
self._resolve_relative_priorities()
|
||||||
|
|
||||||
filtered_injectors = self.filtered_injectors(allow_async, allow_sync)
|
filtered_injectors = self.filtered_injectors(allow_async, allow_sync)
|
||||||
pipeline_callseq = []
|
pipeline_callseq = []
|
||||||
orig_from_types = tuple(from_types)
|
orig_from_types = tuple(from_types)
|
||||||
@@ -91,8 +96,16 @@ class ConvRepo:
|
|||||||
def add_injector(self,
|
def add_injector(self,
|
||||||
func: Callable,
|
func: Callable,
|
||||||
rettype: Optional[type] = None,
|
rettype: Optional[type] = None,
|
||||||
type_remap: Optional[dict[str, type]] = None):
|
type_remap: Optional[dict[str, type]] = None,
|
||||||
self.add_conversion_points(ConversionPoint.from_fn(func, rettype=rettype, type_remap=type_remap))
|
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]:
|
def _callseq_from_callgraph(self, cg: Callgraph) -> list[ConversionPoint]:
|
||||||
if len(cg.variants) == 0:
|
if len(cg.variants) == 0:
|
||||||
@@ -209,8 +222,11 @@ class ConvRepo:
|
|||||||
reason="force_async=True requires allow_async=True"
|
reason="force_async=True requires allow_async=True"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Разрешаем относительные приоритеты
|
||||||
|
self._resolve_relative_priorities()
|
||||||
|
|
||||||
filtered_injectors = self.filtered_injectors(allow_async, allow_sync)
|
filtered_injectors = self.filtered_injectors(allow_async, allow_sync)
|
||||||
|
|
||||||
callseq = self.get_callseq(
|
callseq = self.get_callseq(
|
||||||
filtered_injectors,
|
filtered_injectors,
|
||||||
frozenset(from_types),
|
frozenset(from_types),
|
||||||
@@ -223,13 +239,51 @@ class ConvRepo:
|
|||||||
setattr(ret_fn, '__breakshaft_callseq__', callseq)
|
setattr(ret_fn, '__breakshaft_callseq__', callseq)
|
||||||
return ret_fn
|
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):
|
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 func
|
||||||
|
|
||||||
return inner
|
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:
|
def fork(self, fork_with: Optional[set[ConversionPoint]] = None) -> ConvRepo:
|
||||||
return ForkedConvRepo(self, fork_with or None,
|
return ForkedConvRepo(self, fork_with or None,
|
||||||
self.walker,
|
self.walker,
|
||||||
|
|||||||
@@ -138,6 +138,7 @@ class GraphWalker:
|
|||||||
-> list[CallgraphVariant]:
|
-> list[CallgraphVariant]:
|
||||||
|
|
||||||
if relevance_metric is None:
|
if relevance_metric is None:
|
||||||
|
# Сначала применяем стандартные метрики
|
||||||
template_metrics = [
|
template_metrics = [
|
||||||
lambda x: len(x.consumed_from_types),
|
lambda x: len(x.consumed_from_types),
|
||||||
lambda x: x.consumed_cumsum,
|
lambda x: x.consumed_cumsum,
|
||||||
@@ -151,10 +152,25 @@ class GraphWalker:
|
|||||||
if len(new_variants) > 0:
|
if len(new_variants) > 0:
|
||||||
variants = new_variants
|
variants = new_variants
|
||||||
|
|
||||||
|
# Если всё ещё несколько вариантов, используем приоритеты
|
||||||
if len(variants) > 1:
|
if len(variants) > 1:
|
||||||
# sorting by first injector func name for creating minimal cosistancy
|
# Вычисляем aggregate priority для каждого варианта (сумма приоритетов всех инжекторов в пути)
|
||||||
# could lead to heizenbugs due to incosistancy in path selection between calls
|
def get_aggregate_priority(variant: CallgraphVariant) -> float:
|
||||||
variants.sort(key=lambda x: universal_qualname(x.injector.fn))
|
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
|
return variants
|
||||||
|
|
||||||
if len(variants) < 2:
|
if len(variants) < 2:
|
||||||
|
|||||||
@@ -17,11 +17,23 @@ from .exceptions import MissingReturnType, MissingParamType
|
|||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class ConversionPoint:
|
class ConversionPoint:
|
||||||
|
"""
|
||||||
|
Точка преобразования типов.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
fn: Функция-инжектор
|
||||||
|
injects: Тип, который производит инжектор
|
||||||
|
rettype: Фактический тип возврата функции
|
||||||
|
requires: Обязательные типы аргументов
|
||||||
|
opt_args: Опциональные типы аргументов (с default)
|
||||||
|
priority: Приоритет инжектора (float, по умолчанию 0.0)
|
||||||
|
"""
|
||||||
fn: Callable
|
fn: Callable
|
||||||
injects: type
|
injects: type
|
||||||
rettype: type
|
rettype: type
|
||||||
requires: tuple[type, ...]
|
requires: tuple[type, ...]
|
||||||
opt_args: tuple[type, ...]
|
opt_args: tuple[type, ...]
|
||||||
|
priority: float = 0.0
|
||||||
|
|
||||||
def copy_with(self, **kwargs):
|
def copy_with(self, **kwargs):
|
||||||
fn = kwargs.get('fn', self.fn)
|
fn = kwargs.get('fn', self.fn)
|
||||||
@@ -29,7 +41,8 @@ class ConversionPoint:
|
|||||||
injects = kwargs.get('injects', self.injects)
|
injects = kwargs.get('injects', self.injects)
|
||||||
requires = kwargs.get('requires', self.requires)
|
requires = kwargs.get('requires', self.requires)
|
||||||
opt_args = kwargs.get('opt_args', self.opt_args)
|
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):
|
def __hash__(self):
|
||||||
return hash((self.fn, self.injects, self.requires))
|
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