Files
breakshaft/README.md

296 lines
11 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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
```