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:
Qwen Code Assistant
2026-03-28 14:09:25 +00:00
parent ca605001b3
commit 4c1568fd47
8 changed files with 1316 additions and 10 deletions

View File

@@ -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",

View File

@@ -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,

View File

@@ -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:

View File

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

View 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",
]

View 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",
]