296 lines
11 KiB
Markdown
296 lines
11 KiB
Markdown
# 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
|
||
|
||
``` |