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",
|
||||
]
|
||||
Reference in New Issue
Block a user