breakshaft

Генерация преобразований типов на лету

Зачем это нужно:

Базовая задача библиотеки - применение методов внедрения зависимостей типа ()->SomeObject к потребителям (*,dep_n:SomeObject,*)->*
Однако, кроме прямых применений внедрения зависимостей, библиотека позволяет выстраивать цепочки преобразований (Any)->A | (A)->B | (B)->C | consumer(A,B,C)


Особенности библиотеки:

  • Выстраивает индивидуальный граф всех возможных преобразований для каждого потребителя
  • Генерирует метод подготовки зависимостей по запросу
  • Поддерживает асинхронный контекст
  • Поддерживает внедрение зависимости через синхронные/асинхронные менеджеры контекста
  • Поддерживает Union-типы в зависимостях
  • Учитывает default-параметры
  • Позволяет выстраивать конвейеры преобразований
  • Опционально разворачивает кортежи в возвращаемых значениях

Ограничения библиотеки:

  • Выбор графа преобразований вызывает комбинаторный взрыв
  • Кэширование графов преобразований не поддерживается
  • При некоммутативности сгенерированного графа, имеется опасность неконсистентного выбора пути, поскольку порядок обхода методов, а также графа, не гарантирован
  • Обращение к методам преобразования в сгенерированном методе происходит посредством словаря ссылок - ожидается изменение поведения
  • Поддерживается только in-time генерация методов

Базовое применение:

from dataclasses import dataclass
from breakshaft.convertor import ConvRepo


# Объявляем объекты, с которыми будем работать
@dataclass
class A:
    a: int


@dataclass
class B:
    b: float


repo = ConvRepo()


# Добавляем в репозиторий все преобразования типов, которые можно использовать
@repo.mark_injector()
def b_to_a(b: B) -> A:
    return A(int(b.b))


@repo.mark_injector()
def a_to_b(a: A) -> B:
    return B(float(a.a))


@repo.mark_injector()
def int_to_a(i: int) -> A:
    return A(i)


@repo.mark_injector()
def int_to_b(i: int) -> B:
    return B(float(i))


@repo.mark_injector()
def a_to_int(a: A) -> int:
    return a.a


@repo.mark_injector()
def b_to_int(b: B) -> int:
    return int(b.b)


# Целевая функция
def consumer(dep: A) -> int:
    return dep.a


fn = repo.get_conversion(
    (B,),  # Аргументы генерируемого метода
    consumer,  # Целевая функция, которая будет вызвана
    # Параметры построения графа преобразований
    force_commutative=True,
    # Выбрасывать ошибку если невозможно выделить эксклюзивную последовательность преобразований (default=True)
    allow_async=False,  # Запретить использовать асинхронные преобразования (default=True)
    allow_sync=True,  # Разрешить использовать синхронные преобразования (default=True)
    # Параметры генерации итогового метода
    force_async=False,  # Не требовать генерировать асинхронный метод (default=False)
)

tst = fn(B(1.1))
assert tst == 1


Сборка конвейеров преобразований:

Пусть, имеется несколько методов-потребителей, которые необходимо вызывать последовательно:


from breakshaft.convertor import ConvRepo

repo = ConvRepo()

# Объявляем A и B, а также методы преобразований - как в прошлом примере

type cons2ret = str  # избегаем использования builtin-типов, чтобы избежать простых коллизий 


def consumer1(dep: A) -> B:
    return B(float(42))


def consumer2(dep: B) -> cons2ret:
    return str(dep.b)


def consumer3(dep: cons2ret) -> int:
    return int(float(dep))


pipeline = repo.create_pipeline(
    (B,),
    [consumer1, consumer2, consumer3],
    force_commutative=True,
    allow_sync=True,
    allow_async=False,
    force_async=False
)

dat = pipeline(B(42))
assert dat == 42

Как получить граф преобразований:

from breakshaft.graph_walker import GraphWalker
from breakshaft.convertor import ConvRepo
from breakshaft import util_mermaid  # методы для отрисовки графов в mermaid flowchart

repo = ConvRepo()
# Объявляем A и B, а также методы преобразований - как в прошлом примере
walker = GraphWalker()
from_types = (int,)
g = walker.generate_callgraph(
    repo.convertor_set,  # множество методов преобразований 
    frozenset(from_types),  # все "внутренние" механизмы используют frozenset
    consumer,  # "целевой" метод 
)

Граф с выбором путей

util_mermaid.draw_callgraph_mermaid(
    g,  # граф преобразований 
    split_duplicates=True  # не "склеивать" дублирующиеся хвосты ветвей
)

flowchart TD

%%defs:
    4637413733674907887["3"]
    _2014196705430320452("\(\< class \'\_\_main\_\_.A\'\> \) \-\> NoneType: consumer \[0\]")
    0_0_26854043587429446["branch select \[1\] \[2\]"]
    0_0__7470993180194732607("\(\< class \'\_\_main\_\_.B\'\> \) \-\> A: b\_to\_a \[0\]")
    0_0_0_0_7892409774476285690["1"]
    0_0_0_0__2352457504263425844("\(\< class \'int\'\> \) \-\> B: int\_to\_b \[1\]")
    0_0_0_0_0_0_133146708735736(((0)))
    0_0_7688981453858411382("\(\< class \'int\'\> \) \-\> A: int\_to\_a \[1\]")
    1_0_0_0_133146708735736(((0)))
%%edges:
    head(((head))) --> 4637413733674907887
    0_0__7470993180194732607 ---> 0_0_0_0_7892409774476285690
    0_0_26854043587429446 -.-> 0_0_7688981453858411382
    _2014196705430320452 ---> 0_0_26854043587429446
    0_0_7688981453858411382 ---> 1_0_0_0_133146708735736
    0_0_0_0_7892409774476285690 -.-> 0_0_0_0__2352457504263425844
    0_0_0_0__2352457504263425844 ---> 0_0_0_0_0_0_133146708735736
    4637413733674907887 -.-> _2014196705430320452
    0_0_26854043587429446 -.-> 0_0__7470993180194732607

Далее, выделяется множество всех комбинаций выборов путей

Warning

В этом месте происходит комбинаторный взрыв

exploded = walker.explode_callgraph_branches(g, frozenset(from_types))
print('flowchart TD')
for s_i, selected in enumerate(exploded):
    print(
        util_mermaid.draw_callgraph_mermaid(
            # explode возвращает множество вариантов, а отрисовка принимает только целиковый граф
            Callgraph(frozenset({selected})),
            split_duplicates=True,
            # Не печатать `flowchart TD`
            skip_title=True,
            # каждому подграфу будет добавляться префикс, необходимо для "разделения" отрисовки по ветвям
            prefix=f'{s_i}_'
        )
    )
flowchart TD

%%defs:
    0_6479277657578663668["3"]
    0_5230748712811156587("\(\< class \'\_\_main\_\_.A\'\> \) \-\> NoneType: consumer \[1\]")
    0_0_0_3895278563721661331["2"]
    0_0_0__7339131078346638087("\(\< class \'\_\_main\_\_.B\'\> \) \-\> A: b\_to\_a \[1\]")
    0_0_0_0_0__8039019063582738327["1"]
    0_0_0_0_0__4308564652945379484("\(\< class \'int\'\> \) \-\> B: int\_to\_b \[1\]")
%%edges:
    head(((head))) --> 0_6479277657578663668
    0_0_0_3895278563721661331 -.-> 0_0_0__7339131078346638087
    0_0_0_0_0__8039019063582738327 -.-> 0_0_0_0_0__4308564652945379484
    0_5230748712811156587 ---> 0_0_0_3895278563721661331
    0_0_0__7339131078346638087 ---> 0_0_0_0_0__8039019063582738327
    0_6479277657578663668 -.-> 0_5230748712811156587
%%defs:
    1__220022767842101243["2"]
    1__4215159630864764736("\(\< class \'\_\_main\_\_.A\'\> \) \-\> NoneType: consumer \[1\]")
    0_0_1_3788926318971690339["1"]
    0_0_1_5732874305176457742("\(\< class \'int\'\> \) \-\> A: int\_to\_a \[1\]")
%%edges:
    head(((head))) --> 1__220022767842101243
    1__220022767842101243 -.-> 1__4215159630864764736
    1__4215159630864764736 ---> 0_0_1_3788926318971690339
    0_0_1_3788926318971690339 -.-> 0_0_1_5732874305176457742

Далее, из всех вариантов отбрасываются те, в которых меньше всего на пути преобразования используются исходные объекты, а из них отбрасываются самые длинные пути

Warning

На этом этапе можно наткнуться на некоммутативность, а также неконсистентность выбора пути преобразований в случае если в результате фильтрации получилось больше одного варианта

filtered = walker.filter_exploded_callgraph_branch(exploded)
print('flowchart TD')
for s_i, selected in enumerate(exploded):
    print(
        util_mermaid.draw_callgraph_mermaid(
            Callgraph(frozenset({selected})),
            split_duplicates=True,
            skip_title=True,
            prefix=f'{s_i}_'
        )
    )
flowchart TD

%%defs:
    0_6479277657578663668["3"]
    0_5230748712811156587("\(\< class \'\_\_main\_\_.A\'\> \) \-\> NoneType: consumer \[1\]")
    0_0_0_3895278563721661331["2"]
    0_0_0__7339131078346638087("\(\< class \'\_\_main\_\_.B\'\> \) \-\> A: b\_to\_a \[1\]")
    0_0_0_0_0__8039019063582738327["1"]
    0_0_0_0_0__4308564652945379484("\(\< class \'int\'\> \) \-\> B: int\_to\_b \[1\]")
%%edges:
    head(((head))) --> 0_6479277657578663668
    0_0_0_3895278563721661331 -.-> 0_0_0__7339131078346638087
    0_0_0_0_0__8039019063582738327 -.-> 0_0_0_0_0__4308564652945379484
    0_5230748712811156587 ---> 0_0_0_3895278563721661331
    0_0_0__7339131078346638087 ---> 0_0_0_0_0__8039019063582738327
    0_6479277657578663668 -.-> 0_5230748712811156587

Description
Генерация преобразований типов на лету
Readme LGPL-3.0 222 KiB
Languages
Python 97.6%
Jinja 2.4%