# breakshaft ### Генерация преобразований типов на лету #### Зачем это нужно: Базовая задача библиотеки - применение методов внедрения зависимостей типа `()->SomeObject` к потребителям `(*,dep_n:SomeObject,*)->*` Однако, кроме прямых применений внедрения зависимостей, библиотека позволяет выстраивать цепочки преобразований `(Any)->A | (A)->B | (B)->C | consumer(A,B,C)` ---- #### Особенности библиотеки: - Выстраивает индивидуальный граф всех возможных преобразований для каждого потребителя - Генерирует метод подготовки зависимостей по запросу - Поддерживает асинхронный контекст - Поддерживает внедрение зависимости через синхронные/асинхронные менеджеры контекста - Поддерживает `Union`-типы в зависимостях - Учитывает default-параметры - Позволяет выстраивать конвейеры преобразований - Опционально разворачивает кортежи в возвращаемых значениях #### Ограничения библиотеки: - Выбор графа преобразований вызывает комбинаторный взрыв - Кэширование графов преобразований не поддерживается - При некоммутативности сгенерированного графа, имеется опасность неконсистентного выбора пути, поскольку порядок обхода методов, а также графа, не гарантирован - Обращение к методам преобразования в сгенерированном методе происходит посредством словаря ссылок - ожидается изменение поведения - Поддерживается только in-time генерация методов ---- #### Базовое применение: ```python 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 ``` ---- #### Сборка конвейеров преобразований: Пусть, имеется несколько методов-потребителей, которые необходимо вызывать последовательно: ```python 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 ``` ---- #### Как получить граф преобразований: ```python 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, # "целевой" метод ) ``` #### Граф с выбором путей ```python util_mermaid.draw_callgraph_mermaid( g, # граф преобразований split_duplicates=True # не "склеивать" дублирующиеся хвосты ветвей ) ``` ```mermaid 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** > В этом месте происходит комбинаторный взрыв ```python 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}_' ) ) ``` ```mermaid 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** > На этом этапе можно наткнуться на некоммутативность, а также неконсистентность выбора пути преобразований в случае > если в результате фильтрации получилось больше одного варианта ```python 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}_' ) ) ``` ```mermaid 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 ```