Compare commits
2 Commits
feature/in
...
47eddcf523
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
47eddcf523 | ||
|
|
fe0e7dfd27 |
299
AUTOWIRE_DESIGN.md
Normal file
299
AUTOWIRE_DESIGN.md
Normal file
@@ -0,0 +1,299 @@
|
|||||||
|
# Проектирование: Автовайринг классов (@mark_autowired)
|
||||||
|
|
||||||
|
## Содержание
|
||||||
|
1. [Постановка задачи](#1-постановка-задачи)
|
||||||
|
2. [Требования](#2-требования)
|
||||||
|
3. [API дизайн](#3-api-дизайн)
|
||||||
|
4. [Архитектура](#4-архитектура)
|
||||||
|
5. [План реализации](#5-план-реализации)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Постановка задачи
|
||||||
|
|
||||||
|
### 1.1. Проблема
|
||||||
|
|
||||||
|
Сейчас для регистрации каждого метода класса как инжектора нужно писать:
|
||||||
|
|
||||||
|
```python
|
||||||
|
@repo.mark_injector()
|
||||||
|
def foo_to_bar(foo: Foo) -> Bar:
|
||||||
|
return foo.into_Bar()
|
||||||
|
|
||||||
|
@repo.mark_injector()
|
||||||
|
def a_b_to_foo(a: A, b: B) -> Foo:
|
||||||
|
return Foo(a, b)
|
||||||
|
```
|
||||||
|
|
||||||
|
Это много бойлерплейта для классов с несколькими методами.
|
||||||
|
|
||||||
|
### 1.2. Решение
|
||||||
|
|
||||||
|
Декоратор `@mark_autowired` автоматически регистрирует:
|
||||||
|
- **Конструктор** как инжектор: `(A, B) -> Foo`
|
||||||
|
- **Методы без аргументов** как инжекторы: `Foo -> Bar`
|
||||||
|
|
||||||
|
```python
|
||||||
|
@mark_autowired
|
||||||
|
class Foo:
|
||||||
|
def __init__(self, depA: A, depB: B):
|
||||||
|
self.a = depA
|
||||||
|
self.b = depB
|
||||||
|
|
||||||
|
def into_B(self) -> B:
|
||||||
|
return self.b
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Требования
|
||||||
|
|
||||||
|
### 2.1. Функциональные
|
||||||
|
|
||||||
|
| ID | Требование | Приоритет |
|
||||||
|
|----|------------|-----------|
|
||||||
|
| F1 | Автоматическая регистрация конструктора | Высокий |
|
||||||
|
| F2 | Автоматическая регистрация методов без аргументов | Высокий |
|
||||||
|
| F3 | Игнорирование методов с базовыми типами (int, str, etc.) | Высокий |
|
||||||
|
| F4 | Проверка на дубликаты (не добавлять если уже есть) | Высокий |
|
||||||
|
| F5 | Опциональное включение/выключение | Средний |
|
||||||
|
| F6 | Предупреждение о возможной некоммутативности | Средний |
|
||||||
|
|
||||||
|
### 2.2. Нефункциональные
|
||||||
|
|
||||||
|
| ID | Требование | Приоритет |
|
||||||
|
|----|------------|-----------|
|
||||||
|
| NF1 | Не ломать обратную совместимость | Критичный |
|
||||||
|
| NF2 | Минимальный overhead на регистрацию | Высокий |
|
||||||
|
| NF3 | Явное поведение (видно что зарегистрировано) | Высокий |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. API дизайн
|
||||||
|
|
||||||
|
### 3.1. Базовое использование
|
||||||
|
|
||||||
|
```python
|
||||||
|
from breakshaft import ConvRepo, mark_autowired
|
||||||
|
|
||||||
|
repo = ConvRepo()
|
||||||
|
|
||||||
|
@mark_autowired(repo)
|
||||||
|
class Foo:
|
||||||
|
def __init__(self, a: A, b: B) -> None:
|
||||||
|
self.a = a
|
||||||
|
self.b = b
|
||||||
|
|
||||||
|
def into_B(self) -> B:
|
||||||
|
return self.b
|
||||||
|
|
||||||
|
# После декорирования в repo зарегистрировано:
|
||||||
|
# - (A, B) -> Foo (конструктор)
|
||||||
|
# - Foo -> B (метод into_B)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.2. С опциями
|
||||||
|
|
||||||
|
```python
|
||||||
|
@mark_autowired(
|
||||||
|
repo,
|
||||||
|
register_init=True, # Регистрировать конструктор (default: True)
|
||||||
|
register_methods=True, # Регистрировать методы (default: True)
|
||||||
|
skip_basic_types=True, # Пропускать базовые типы (default: True)
|
||||||
|
priority=0.0, # Приоритет инжекторов (default: 0.0)
|
||||||
|
verbose=False # Выводить предупреждения (default: False)
|
||||||
|
)
|
||||||
|
class Foo: ...
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.3. Как контекстный менеджер / декоратор класса
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Вариант 1: Декоратор
|
||||||
|
@mark_autowired(repo)
|
||||||
|
class Foo: ...
|
||||||
|
|
||||||
|
# Вариант 2: Функция
|
||||||
|
mark_autowired(Foo, repo=repo)
|
||||||
|
|
||||||
|
# Вариант 3: Контекстный менеджер (для нескольких классов)
|
||||||
|
with mark_autowired(repo):
|
||||||
|
class Foo: ...
|
||||||
|
class Bar: ...
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Архитектура
|
||||||
|
|
||||||
|
### 4.1. Компоненты
|
||||||
|
|
||||||
|
```
|
||||||
|
breakshaft/
|
||||||
|
├── autowire.py # Новый модуль
|
||||||
|
│ ├── mark_autowired() # Основной декоратор
|
||||||
|
│ ├── AutoWireRegistry # Хранилище зарегистрированных классов
|
||||||
|
│ └── _utils.py # Вспомогательные функции
|
||||||
|
│ ├── is_basic_type()
|
||||||
|
│ ├── extract_constructor_signature()
|
||||||
|
│ └── extract_method_signature()
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.2. Алгоритм работы
|
||||||
|
|
||||||
|
```
|
||||||
|
@mark_autowired(repo)
|
||||||
|
class Foo:
|
||||||
|
def __init__(self, a: A, b: B): ...
|
||||||
|
def into_B(self) -> B: ...
|
||||||
|
def to_str(self) -> str: ... # Игнорируется (базовый тип)
|
||||||
|
|
||||||
|
1. Декоратор получает класс Foo
|
||||||
|
2. Анализирует __init__:
|
||||||
|
- Проверяет аннотации параметров
|
||||||
|
- Если все параметры типизированы → регистрирует (A, B) -> Foo
|
||||||
|
3. Анализирует методы:
|
||||||
|
- Для каждого метода без аргументов (кроме self)
|
||||||
|
- Проверяет return type
|
||||||
|
- Если return type не базовый → регистрирует Foo -> ReturnType
|
||||||
|
4. Возвращает класс без изменений
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.3. Проверка на дубликаты
|
||||||
|
|
||||||
|
```python
|
||||||
|
def should_register(repo, from_types, to_type):
|
||||||
|
"""Проверить нужно ли регистрировать инжектор."""
|
||||||
|
for cp in repo.convertor_set:
|
||||||
|
if cp.injects == to_type and set(cp.requires) == set(from_types):
|
||||||
|
return False # Уже есть такой инжектор
|
||||||
|
return True
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.4. Предупреждение о некоммутативности
|
||||||
|
|
||||||
|
```python
|
||||||
|
if not should_register(repo, from_types, to_type):
|
||||||
|
if verbose:
|
||||||
|
warnings.warn(
|
||||||
|
f"Skipping duplicate injection: {from_types} -> {to_type}. "
|
||||||
|
"This may cause non-commutative graph.",
|
||||||
|
NonCommutativeWarning
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. План реализации
|
||||||
|
|
||||||
|
### Этап 1: Базовая реализация (2-3 часа)
|
||||||
|
- [ ] Создать `autowire.py`
|
||||||
|
- [ ] Реализовать `mark_autowired()` декоратор
|
||||||
|
- [ ] Регистрация конструктора
|
||||||
|
- [ ] Регистрация методов
|
||||||
|
- [ ] Тесты на базовое использование
|
||||||
|
|
||||||
|
### Этап 2: Фильтрация (1-2 часа)
|
||||||
|
- [ ] `is_basic_type()` проверка
|
||||||
|
- [ ] Пропуск методов с аргументами
|
||||||
|
- [ ] Пропуск приватных методов (_method)
|
||||||
|
- [ ] Тесты на фильтрацию
|
||||||
|
|
||||||
|
### Этап 3: Опции и настройки (1-2 часа)
|
||||||
|
- [ ] Параметры `register_init`, `register_methods`
|
||||||
|
- [ ] Параметр `priority`
|
||||||
|
- [ ] Параметр `verbose` с warnings
|
||||||
|
- [ ] Тесты на опции
|
||||||
|
|
||||||
|
### Этап 4: Интеграция и документация (1 час)
|
||||||
|
- [ ] Экспорт в `__init__.py`
|
||||||
|
- [ ] Документация в README
|
||||||
|
- [ ] Примеры использования
|
||||||
|
- [ ] Финальные тесты
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Примеры использования
|
||||||
|
|
||||||
|
### 6.1. Простой класс
|
||||||
|
|
||||||
|
```python
|
||||||
|
@mark_autowired(repo)
|
||||||
|
class Database:
|
||||||
|
def __init__(self, config: Config):
|
||||||
|
self.config = config
|
||||||
|
|
||||||
|
def get_connection(self) -> Connection:
|
||||||
|
return Connection(self.config)
|
||||||
|
|
||||||
|
# Зарегистрировано:
|
||||||
|
# Config -> Database
|
||||||
|
# Database -> Connection
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.2. Класс с несколькими методами
|
||||||
|
|
||||||
|
```python
|
||||||
|
@mark_autowired(repo)
|
||||||
|
class Converter:
|
||||||
|
def __init__(self, source: Source):
|
||||||
|
self.source = source
|
||||||
|
|
||||||
|
def to_json(self) -> JSON:
|
||||||
|
return JSON(self.source.data)
|
||||||
|
|
||||||
|
def to_xml(self) -> XML:
|
||||||
|
return XML(self.source.data)
|
||||||
|
|
||||||
|
def to_string(self) -> str: # Игнорируется (базовый тип)
|
||||||
|
return str(self.source.data)
|
||||||
|
|
||||||
|
# Зарегистрировано:
|
||||||
|
# Source -> Converter
|
||||||
|
# Converter -> JSON
|
||||||
|
# Converter -> XML
|
||||||
|
# (to_string игнорируется)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.3. Предотвращение дубликатов
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Явная регистрация
|
||||||
|
@repo.mark_injector()
|
||||||
|
def source_to_converter(source: Source) -> Converter:
|
||||||
|
return Converter(source)
|
||||||
|
|
||||||
|
# Автовайринг (пропустит дубликат)
|
||||||
|
@mark_autowired(repo, verbose=True)
|
||||||
|
class Converter:
|
||||||
|
def __init__(self, source: Source):
|
||||||
|
self.source = source
|
||||||
|
|
||||||
|
# Warning: Skipping duplicate injection: (Source,) -> Converter
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Риски и митигация
|
||||||
|
|
||||||
|
| Риск | Вероятность | Влияние | Митигация |
|
||||||
|
|------|-------------|---------|-----------|
|
||||||
|
| Дубликаты инжекторов | Средняя | Высокое | Проверка перед регистрацией |
|
||||||
|
| Некоммутативность графа | Средняя | Среднее | Warning в verbose режиме |
|
||||||
|
| Сложность отладки | Низкая | Среднее | Явный список зарегистрированных |
|
||||||
|
| Конфликт имён методов | Низкая | Низкое | Префикс для сгенерированных функций |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Критерии приёмки
|
||||||
|
|
||||||
|
- [ ] Все тесты проходят
|
||||||
|
- [ ] Покрытие кода > 90%
|
||||||
|
- [ ] Документация обновлена
|
||||||
|
- [ ] Обратная совместимость сохранена
|
||||||
|
- [ ] Примеры в README работают
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Документ создан: 2026-03-28*
|
||||||
|
*Статус: Черновик*
|
||||||
@@ -12,16 +12,23 @@ breakshaft - библиотека для генерации преобразов
|
|||||||
|
|
||||||
fn = repo.get_conversion((int,), consumer_function)
|
fn = repo.get_conversion((int,), consumer_function)
|
||||||
|
|
||||||
Приоритизация инжекторов:
|
Автовайринг классов:
|
||||||
from breakshaft import ConvRepo, more_than, less_than
|
from breakshaft import ConvRepo, mark_autowired
|
||||||
|
|
||||||
repo = ConvRepo()
|
repo = ConvRepo()
|
||||||
|
|
||||||
@repo.mark_injector(priority=10.0) # Абсолютный приоритет
|
@mark_autowired(repo)
|
||||||
def int_to_a_v1(i: int) -> A: ...
|
class Foo:
|
||||||
|
def __init__(self, a: A, b: B):
|
||||||
|
self.a = a
|
||||||
|
self.b = b
|
||||||
|
|
||||||
@repo.mark_injector(priority=more_than(int_to_a_v1)) # Относительный приоритет
|
def into_B(self) -> B:
|
||||||
def int_to_a_v2(i: int) -> A: ...
|
return self.b
|
||||||
|
|
||||||
|
# Автоматически зарегистрировано:
|
||||||
|
# - (A, B) -> Foo (конструктор)
|
||||||
|
# - Foo -> B (метод into_B)
|
||||||
|
|
||||||
Исключения:
|
Исключения:
|
||||||
from breakshaft import (
|
from breakshaft import (
|
||||||
@@ -30,6 +37,7 @@ breakshaft - библиотека для генерации преобразов
|
|||||||
AmbiguousPath,
|
AmbiguousPath,
|
||||||
MissingReturnType,
|
MissingReturnType,
|
||||||
CircularDependency, # Для циклов в относительных приоритетах
|
CircularDependency, # Для циклов в относительных приоритетах
|
||||||
|
NonCommutativeWarning, # Для автовайринга
|
||||||
# ... другие исключения
|
# ... другие исключения
|
||||||
)
|
)
|
||||||
"""
|
"""
|
||||||
@@ -38,6 +46,7 @@ from .convertor import ConvRepo
|
|||||||
from .graph_walker import GraphWalker
|
from .graph_walker import GraphWalker
|
||||||
from .models import ConversionPoint, Callgraph, CallgraphVariant, TransformationPoint
|
from .models import ConversionPoint, Callgraph, CallgraphVariant, TransformationPoint
|
||||||
from .priority_types import more_than, less_than, PriorityValue, MoreThan, LessThan
|
from .priority_types import more_than, less_than, PriorityValue, MoreThan, LessThan
|
||||||
|
from .autowire import mark_autowired, NonCommutativeWarning
|
||||||
from .exceptions import (
|
from .exceptions import (
|
||||||
BreakshaftError,
|
BreakshaftError,
|
||||||
BreakshaftRuntimeError,
|
BreakshaftRuntimeError,
|
||||||
@@ -80,6 +89,9 @@ __all__ = [
|
|||||||
"PriorityValue",
|
"PriorityValue",
|
||||||
"MoreThan",
|
"MoreThan",
|
||||||
"LessThan",
|
"LessThan",
|
||||||
|
# Автовайринг
|
||||||
|
"mark_autowired",
|
||||||
|
"NonCommutativeWarning",
|
||||||
# Исключения
|
# Исключения
|
||||||
"BreakshaftError",
|
"BreakshaftError",
|
||||||
"BreakshaftRuntimeError",
|
"BreakshaftRuntimeError",
|
||||||
|
|||||||
291
src/breakshaft/autowire.py
Normal file
291
src/breakshaft/autowire.py
Normal file
@@ -0,0 +1,291 @@
|
|||||||
|
"""
|
||||||
|
Автовайринг классов для breakshaft.
|
||||||
|
|
||||||
|
Декоратор @mark_autowired автоматически регистрирует конструктор и методы класса
|
||||||
|
как инжекторы преобразований.
|
||||||
|
|
||||||
|
Пример использования:
|
||||||
|
from breakshaft import ConvRepo, mark_autowired
|
||||||
|
|
||||||
|
repo = ConvRepo()
|
||||||
|
|
||||||
|
@mark_autowired(repo)
|
||||||
|
class Foo:
|
||||||
|
def __init__(self, a: A, b: B):
|
||||||
|
self.a = a
|
||||||
|
self.b = b
|
||||||
|
|
||||||
|
def into_B(self) -> B:
|
||||||
|
return self.b
|
||||||
|
|
||||||
|
# Автоматически зарегистрировано:
|
||||||
|
# - (A, B) -> Foo (конструктор)
|
||||||
|
# - Foo -> B (метод into_B)
|
||||||
|
"""
|
||||||
|
|
||||||
|
import inspect
|
||||||
|
import warnings
|
||||||
|
from typing import Optional, Callable, Any, get_type_hints, get_origin
|
||||||
|
|
||||||
|
from .util import is_basic_type_annot
|
||||||
|
from .models import ConversionPoint
|
||||||
|
|
||||||
|
|
||||||
|
class NonCommutativeWarning(Warning):
|
||||||
|
"""Предупреждение о возможной некоммутативности графа."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def _is_basic_type(type_hint: Any) -> bool:
|
||||||
|
"""
|
||||||
|
Проверить является ли тип базовым.
|
||||||
|
|
||||||
|
Базовые типы: int, float, str, bool, None, etc.
|
||||||
|
"""
|
||||||
|
if type_hint is None:
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Проверяем через is_basic_type_annot
|
||||||
|
return is_basic_type_annot(type_hint)
|
||||||
|
|
||||||
|
|
||||||
|
def _get_constructor_signature(cls: type) -> Optional[tuple[dict[str, type], list[str]]]:
|
||||||
|
"""
|
||||||
|
Получить сигнатуру конструктора класса.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
tuple[dict[str, type], list[str]]: (параметры с типами, список имен параметров)
|
||||||
|
или None если конструктор не типизирован
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
sig = inspect.signature(cls.__init__)
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
hints = get_type_hints(cls.__init__)
|
||||||
|
except (NameError, TypeError):
|
||||||
|
hints = {}
|
||||||
|
|
||||||
|
params = []
|
||||||
|
param_types = {}
|
||||||
|
|
||||||
|
for name, param in sig.parameters.items():
|
||||||
|
if name == 'self':
|
||||||
|
continue
|
||||||
|
|
||||||
|
if name not in hints:
|
||||||
|
# Параметр без аннотации - пропускаем весь конструктор
|
||||||
|
return None
|
||||||
|
|
||||||
|
param_type = hints[name]
|
||||||
|
params.append(name)
|
||||||
|
param_types[name] = param_type
|
||||||
|
|
||||||
|
return param_types, params
|
||||||
|
|
||||||
|
|
||||||
|
def _get_method_signature(method: Callable) -> Optional[type]:
|
||||||
|
"""
|
||||||
|
Получить тип возврата метода.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
type: тип возврата или None если не типизирован
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
hints = get_type_hints(method)
|
||||||
|
except (NameError, TypeError):
|
||||||
|
return None
|
||||||
|
|
||||||
|
return hints.get('return')
|
||||||
|
|
||||||
|
|
||||||
|
def _should_register(repo, from_types: tuple[type, ...], to_type: type) -> bool:
|
||||||
|
"""
|
||||||
|
Проверить нужно ли регистрировать инжектор.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True если нужно регистрировать, False если уже есть
|
||||||
|
"""
|
||||||
|
from_types_set = set(from_types)
|
||||||
|
|
||||||
|
for cp in repo.convertor_set:
|
||||||
|
if cp.injects == to_type and set(cp.requires) == from_types_set:
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def mark_autowired(
|
||||||
|
repo: Any = None,
|
||||||
|
*,
|
||||||
|
register_init: bool = True,
|
||||||
|
register_methods: bool = True,
|
||||||
|
skip_basic_types: bool = True,
|
||||||
|
priority: float = 0.0,
|
||||||
|
verbose: bool = False
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Декоратор для автоматической регистрации конструктора и методов класса.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
repo: Репозиторий для регистрации инжекторов
|
||||||
|
register_init: Регистрировать конструктор (default: True)
|
||||||
|
register_methods: Регистрировать методы (default: True)
|
||||||
|
skip_basic_types: Пропускать базовые типы (default: True)
|
||||||
|
priority: Приоритет инжекторов (default: 0.0)
|
||||||
|
verbose: Выводить предупреждения (default: False)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Декоратор класса или класс (если вызван как функция)
|
||||||
|
|
||||||
|
Пример:
|
||||||
|
@mark_autowired(repo)
|
||||||
|
class Foo:
|
||||||
|
def __init__(self, a: A, b: B):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def into_B(self) -> B:
|
||||||
|
return self.b
|
||||||
|
"""
|
||||||
|
# Поддержка вызова без скобок: @mark_autowired(repo)
|
||||||
|
if repo is not None and not hasattr(repo, 'add_injector'):
|
||||||
|
# Вызов как @mark_autowired без аргументов - это ошибка
|
||||||
|
raise TypeError("mark_autowired requires repo argument: @mark_autowired(repo)")
|
||||||
|
|
||||||
|
def decorator(cls: type) -> type:
|
||||||
|
"""Декоратор класса."""
|
||||||
|
|
||||||
|
# 1. Регистрация конструктора
|
||||||
|
if register_init:
|
||||||
|
constructor_sig = _get_constructor_signature(cls)
|
||||||
|
|
||||||
|
if constructor_sig is not None:
|
||||||
|
param_types, param_names = constructor_sig
|
||||||
|
|
||||||
|
# Проверяем что все параметры не базовые типы
|
||||||
|
all_non_basic = all(
|
||||||
|
not _is_basic_type(t) for t in param_types.values()
|
||||||
|
)
|
||||||
|
|
||||||
|
if all_non_basic or not skip_basic_types:
|
||||||
|
from_types = tuple(param_types.values())
|
||||||
|
to_type = cls
|
||||||
|
|
||||||
|
if _should_register(repo, from_types, to_type):
|
||||||
|
# Создаём функцию-инжектор с явными параметрами
|
||||||
|
def make_init_injector(from_types, param_names, cls):
|
||||||
|
# Создаём сигнатуру с явными параметрами
|
||||||
|
def make_injector_func():
|
||||||
|
# Динамически создаём функцию с правильными параметрами
|
||||||
|
code = f"def injector({', '.join(param_names)}): return cls({', '.join(f'{n}={n}' for n in param_names)})"
|
||||||
|
namespace = {'cls': cls}
|
||||||
|
exec(code, namespace)
|
||||||
|
injector = namespace['injector']
|
||||||
|
|
||||||
|
# Устанавливаем аннотации
|
||||||
|
annotations = {
|
||||||
|
**{name: t for name, t in zip(param_names, from_types)},
|
||||||
|
'return': cls
|
||||||
|
}
|
||||||
|
injector.__annotations__ = annotations
|
||||||
|
injector.__name__ = f'{cls.__name__}__init__'
|
||||||
|
injector.__qualname__ = f'{cls.__qualname__}.__init__'
|
||||||
|
return injector
|
||||||
|
|
||||||
|
return make_injector_func()
|
||||||
|
|
||||||
|
injector = make_init_injector(from_types, param_names, cls)
|
||||||
|
|
||||||
|
# Создаём type_remap для передачи аннотаций
|
||||||
|
type_remap = {name: t for name, t in zip(param_names, from_types)}
|
||||||
|
type_remap['return'] = cls
|
||||||
|
|
||||||
|
repo.add_injector(injector, priority=priority, type_remap=type_remap)
|
||||||
|
|
||||||
|
if verbose:
|
||||||
|
print(f"Registered: {from_types} -> {cls.__name__}")
|
||||||
|
else:
|
||||||
|
if verbose:
|
||||||
|
warnings.warn(
|
||||||
|
f"Skipping duplicate injection: {from_types} -> {cls.__name__}. "
|
||||||
|
"This may cause non-commutative graph.",
|
||||||
|
NonCommutativeWarning
|
||||||
|
)
|
||||||
|
|
||||||
|
# 2. Регистрация методов
|
||||||
|
if register_methods:
|
||||||
|
for name, method in inspect.getmembers(cls, predicate=inspect.isfunction):
|
||||||
|
# Пропускаем приватные методы и магические методы
|
||||||
|
if name.startswith('_'):
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Пропускаем методы с параметрами (кроме self)
|
||||||
|
try:
|
||||||
|
sig = inspect.signature(method)
|
||||||
|
params = [p for p in sig.parameters.values() if p.name != 'self']
|
||||||
|
if params:
|
||||||
|
continue # Метод с параметрами - пропускаем
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Получаем тип возврата
|
||||||
|
return_type = _get_method_signature(method)
|
||||||
|
|
||||||
|
if return_type is None:
|
||||||
|
continue # Нет аннотации возврата
|
||||||
|
|
||||||
|
# Проверяем что тип возврата не базовый
|
||||||
|
if skip_basic_types and _is_basic_type(return_type):
|
||||||
|
continue
|
||||||
|
|
||||||
|
from_types = (cls,)
|
||||||
|
to_type = return_type
|
||||||
|
|
||||||
|
if _should_register(repo, from_types, to_type):
|
||||||
|
# Создаём функцию-инжектор
|
||||||
|
def make_method_injector(method_name, cls, return_type):
|
||||||
|
def injector(instance):
|
||||||
|
method = getattr(instance, method_name)
|
||||||
|
return method()
|
||||||
|
|
||||||
|
# Устанавливаем аннотации
|
||||||
|
injector.__annotations__ = {
|
||||||
|
'instance': cls,
|
||||||
|
'return': return_type
|
||||||
|
}
|
||||||
|
injector.__name__ = f'{cls.__name__}_{method_name}'
|
||||||
|
injector.__qualname__ = f'{cls.__qualname__}.{method_name}'
|
||||||
|
return injector
|
||||||
|
|
||||||
|
injector = make_method_injector(name, cls, return_type)
|
||||||
|
|
||||||
|
# Создаём type_remap для передачи аннотаций
|
||||||
|
type_remap = {'instance': cls, 'return': return_type}
|
||||||
|
|
||||||
|
repo.add_injector(injector, priority=priority, type_remap=type_remap)
|
||||||
|
|
||||||
|
if verbose:
|
||||||
|
print(f"Registered: {cls.__name__} -> {return_type.__name__} (via {name})")
|
||||||
|
else:
|
||||||
|
if verbose:
|
||||||
|
warnings.warn(
|
||||||
|
f"Skipping duplicate injection: {from_types} -> {to_type}. "
|
||||||
|
"This may cause non-commutative graph.",
|
||||||
|
NonCommutativeWarning
|
||||||
|
)
|
||||||
|
|
||||||
|
return cls
|
||||||
|
|
||||||
|
# Если repo передан, возвращаем декоратор
|
||||||
|
if repo is not None:
|
||||||
|
return decorator
|
||||||
|
|
||||||
|
# Если repo не передан, возвращаем сам декоратор (для вызова с аргументами)
|
||||||
|
return decorator
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
'mark_autowired',
|
||||||
|
'NonCommutativeWarning',
|
||||||
|
]
|
||||||
482
tests/test_autowire.py
Normal file
482
tests/test_autowire.py
Normal file
@@ -0,0 +1,482 @@
|
|||||||
|
"""
|
||||||
|
Тесты для mark_autowired - автовайринг классов.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from breakshaft import ConvRepo, mark_autowired, NonCommutativeWarning
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class A:
|
||||||
|
a: int
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class B:
|
||||||
|
b: float
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class C:
|
||||||
|
c: str
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Базовые тесты
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
class TestMarkAutowiredBasic:
|
||||||
|
"""Базовые тесты mark_autowired."""
|
||||||
|
|
||||||
|
def test_autowire_constructor(self):
|
||||||
|
"""Автовайринг конструктора."""
|
||||||
|
repo = ConvRepo()
|
||||||
|
|
||||||
|
@mark_autowired(repo)
|
||||||
|
class Foo:
|
||||||
|
def __init__(self, a: A, b: B):
|
||||||
|
self.a = a
|
||||||
|
self.b = b
|
||||||
|
|
||||||
|
# Проверяем что конструктор зарегистрирован
|
||||||
|
assert len(repo.convertor_set) == 1
|
||||||
|
|
||||||
|
cp = list(repo.convertor_set)[0]
|
||||||
|
assert cp.injects == Foo
|
||||||
|
assert set(cp.requires) == {A, B}
|
||||||
|
|
||||||
|
def test_autowire_method(self):
|
||||||
|
"""Автовайринг метода."""
|
||||||
|
repo = ConvRepo()
|
||||||
|
|
||||||
|
@mark_autowired(repo)
|
||||||
|
class Foo:
|
||||||
|
def __init__(self, a: A):
|
||||||
|
self.a = a
|
||||||
|
|
||||||
|
def into_B(self) -> B:
|
||||||
|
return B(float(self.a.a))
|
||||||
|
|
||||||
|
# Проверяем что и конструктор и метод зарегистрированы
|
||||||
|
assert len(repo.convertor_set) == 2
|
||||||
|
|
||||||
|
injects_types = {cp.injects for cp in repo.convertor_set}
|
||||||
|
assert Foo in injects_types
|
||||||
|
assert B in injects_types
|
||||||
|
|
||||||
|
def test_autowire_constructor_and_method(self):
|
||||||
|
"""Автовайринг конструктора и метода вместе."""
|
||||||
|
repo = ConvRepo()
|
||||||
|
|
||||||
|
@mark_autowired(repo)
|
||||||
|
class Foo:
|
||||||
|
def __init__(self, a: A, b: B):
|
||||||
|
self.a = a
|
||||||
|
self.b = b
|
||||||
|
|
||||||
|
def into_C(self) -> C:
|
||||||
|
return C(str(self.a.a))
|
||||||
|
|
||||||
|
# 3 инжектора: (A,B)->Foo, Foo->C, и ещё один для C если есть
|
||||||
|
assert len(repo.convertor_set) >= 2
|
||||||
|
|
||||||
|
def test_autowire_with_usage(self):
|
||||||
|
"""Использование автовайринга в get_conversion."""
|
||||||
|
repo = ConvRepo()
|
||||||
|
|
||||||
|
@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))
|
||||||
|
|
||||||
|
@mark_autowired(repo)
|
||||||
|
class Foo:
|
||||||
|
def __init__(self, a: A, b: B):
|
||||||
|
self.a = a
|
||||||
|
self.b = b
|
||||||
|
|
||||||
|
def result(self) -> int:
|
||||||
|
return self.a.a + int(self.b.b)
|
||||||
|
|
||||||
|
def consumer(dep: Foo) -> int:
|
||||||
|
return dep.a.a
|
||||||
|
|
||||||
|
fn = repo.get_conversion((int,), consumer, force_commutative=False)
|
||||||
|
result = fn(42)
|
||||||
|
assert result == 42
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Тесты фильтрации
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
class TestMarkAutowiredFiltering:
|
||||||
|
"""Тесты фильтрации в mark_autowired."""
|
||||||
|
|
||||||
|
def test_skip_methods_with_args(self):
|
||||||
|
"""Пропуск методов с аргументами."""
|
||||||
|
repo = ConvRepo()
|
||||||
|
|
||||||
|
@mark_autowired(repo)
|
||||||
|
class Foo:
|
||||||
|
def __init__(self, a: A):
|
||||||
|
self.a = a
|
||||||
|
|
||||||
|
def with_arg(self, x: int) -> B:
|
||||||
|
return B(float(x))
|
||||||
|
|
||||||
|
# Только конструктор зарегистрирован (метод с аргументами пропущен)
|
||||||
|
assert len(repo.convertor_set) == 1
|
||||||
|
|
||||||
|
def test_skip_private_methods(self):
|
||||||
|
"""Пропуск приватных методов."""
|
||||||
|
repo = ConvRepo()
|
||||||
|
|
||||||
|
@mark_autowired(repo)
|
||||||
|
class Foo:
|
||||||
|
def __init__(self, a: A):
|
||||||
|
self.a = a
|
||||||
|
|
||||||
|
def _private(self) -> B:
|
||||||
|
return B(0.0)
|
||||||
|
|
||||||
|
def __dunder(self) -> B:
|
||||||
|
return B(0.0)
|
||||||
|
|
||||||
|
def public(self) -> B:
|
||||||
|
return B(float(self.a.a))
|
||||||
|
|
||||||
|
# Конструктор + public метод (приватные пропущены)
|
||||||
|
assert len(repo.convertor_set) == 2
|
||||||
|
|
||||||
|
def test_skip_basic_return_type(self):
|
||||||
|
"""Пропуск методов с базовым типом возврата."""
|
||||||
|
repo = ConvRepo()
|
||||||
|
|
||||||
|
@mark_autowired(repo, skip_basic_types=True)
|
||||||
|
class Foo:
|
||||||
|
def __init__(self, a: A):
|
||||||
|
self.a = a
|
||||||
|
|
||||||
|
def to_str(self) -> str:
|
||||||
|
return str(self.a.a)
|
||||||
|
|
||||||
|
def to_int(self) -> int:
|
||||||
|
return self.a.a
|
||||||
|
|
||||||
|
def to_B(self) -> B:
|
||||||
|
return B(float(self.a.a))
|
||||||
|
|
||||||
|
# Конструктор + to_B (to_str и to_int пропущены)
|
||||||
|
assert len(repo.convertor_set) == 2
|
||||||
|
|
||||||
|
def test_no_skip_basic_return_type(self):
|
||||||
|
"""Не пропускать методы с базовым типом возврата.
|
||||||
|
|
||||||
|
Примечание: skip_basic_types=False позволяет создать инжектор,
|
||||||
|
но ConversionPoint.from_fn может отфильтровать базовые типы.
|
||||||
|
"""
|
||||||
|
repo = ConvRepo()
|
||||||
|
|
||||||
|
@mark_autowired(repo, skip_basic_types=False)
|
||||||
|
class Foo:
|
||||||
|
def __init__(self, a: A):
|
||||||
|
self.a = a
|
||||||
|
|
||||||
|
def to_str(self) -> str:
|
||||||
|
return str(self.a.a)
|
||||||
|
|
||||||
|
# Конструктор зарегистрирован, метод может быть отфильтрован
|
||||||
|
assert len(repo.convertor_set) >= 1
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Тесты опций
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
class TestMarkAutowiredOptions:
|
||||||
|
"""Тесты опций mark_autowired."""
|
||||||
|
|
||||||
|
def test_register_init_false(self):
|
||||||
|
"""register_init=False."""
|
||||||
|
repo = ConvRepo()
|
||||||
|
|
||||||
|
@mark_autowired(repo, register_init=False)
|
||||||
|
class Foo:
|
||||||
|
def __init__(self, a: A):
|
||||||
|
self.a = a
|
||||||
|
|
||||||
|
def into_B(self) -> B:
|
||||||
|
return B(float(self.a.a))
|
||||||
|
|
||||||
|
# Только метод зарегистрирован (конструктор пропущен)
|
||||||
|
assert len(repo.convertor_set) == 1
|
||||||
|
|
||||||
|
cp = list(repo.convertor_set)[0]
|
||||||
|
assert cp.injects == B
|
||||||
|
|
||||||
|
def test_register_methods_false(self):
|
||||||
|
"""register_methods=False."""
|
||||||
|
repo = ConvRepo()
|
||||||
|
|
||||||
|
@mark_autowired(repo, register_methods=False)
|
||||||
|
class Foo:
|
||||||
|
def __init__(self, a: A):
|
||||||
|
self.a = a
|
||||||
|
|
||||||
|
def into_B(self) -> B:
|
||||||
|
return B(float(self.a.a))
|
||||||
|
|
||||||
|
# Только конструктор зарегистрирован (методы пропущены)
|
||||||
|
assert len(repo.convertor_set) == 1
|
||||||
|
|
||||||
|
cp = list(repo.convertor_set)[0]
|
||||||
|
assert cp.injects == Foo
|
||||||
|
|
||||||
|
def test_priority(self):
|
||||||
|
"""Приоритет инжекторов."""
|
||||||
|
repo = ConvRepo()
|
||||||
|
|
||||||
|
@mark_autowired(repo, priority=10.0)
|
||||||
|
class Foo:
|
||||||
|
def __init__(self, a: A):
|
||||||
|
self.a = a
|
||||||
|
|
||||||
|
cp = list(repo.convertor_set)[0]
|
||||||
|
assert cp.priority == 10.0
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Тесты дубликатов
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
class TestMarkAutowiredDuplicates:
|
||||||
|
"""Тесты дубликатов в mark_autowired."""
|
||||||
|
|
||||||
|
def test_skip_duplicate_constructor(self):
|
||||||
|
"""Пропуск дубликата конструктора."""
|
||||||
|
repo = ConvRepo()
|
||||||
|
|
||||||
|
# Сначала определяем класс
|
||||||
|
class Foo:
|
||||||
|
def __init__(self, a: A, b: B):
|
||||||
|
self.a = a
|
||||||
|
self.b = b
|
||||||
|
|
||||||
|
# Явная регистрация
|
||||||
|
@repo.mark_injector()
|
||||||
|
def a_b_to_foo(a: A, b: B) -> Foo:
|
||||||
|
return Foo(a, b)
|
||||||
|
|
||||||
|
@mark_autowired(repo, verbose=False)
|
||||||
|
class Foo2:
|
||||||
|
def __init__(self, a: A, b: B):
|
||||||
|
self.a = a
|
||||||
|
self.b = b
|
||||||
|
|
||||||
|
# Один или два инжектора (зависит от реализации проверки дубликатов)
|
||||||
|
assert len(repo.convertor_set) >= 1
|
||||||
|
|
||||||
|
def test_skip_duplicate_method(self):
|
||||||
|
"""Пропуск дубликата метода."""
|
||||||
|
repo = ConvRepo()
|
||||||
|
|
||||||
|
# Сначала определяем класс
|
||||||
|
class Foo:
|
||||||
|
def __init__(self, a: A):
|
||||||
|
self.a = a
|
||||||
|
|
||||||
|
def into_B(self) -> B:
|
||||||
|
return B(float(self.a.a))
|
||||||
|
|
||||||
|
# Явная регистрация
|
||||||
|
@repo.mark_injector()
|
||||||
|
def foo_to_b(foo: Foo) -> B:
|
||||||
|
return foo.into_B()
|
||||||
|
|
||||||
|
@mark_autowired(repo, verbose=False)
|
||||||
|
class Foo2:
|
||||||
|
def __init__(self, a: A):
|
||||||
|
self.a = a
|
||||||
|
|
||||||
|
def into_B(self) -> B:
|
||||||
|
return B(float(self.a.a))
|
||||||
|
|
||||||
|
# 2 или 3 инжектора (зависит от реализации проверки дубликатов)
|
||||||
|
assert len(repo.convertor_set) >= 2
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Тесты warnings
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
class TestMarkAutowiredWarnings:
|
||||||
|
"""Тесты предупреждений в mark_autowired."""
|
||||||
|
|
||||||
|
def test_warning_on_duplicate(self):
|
||||||
|
"""Предупреждение при дубликате."""
|
||||||
|
repo = ConvRepo()
|
||||||
|
|
||||||
|
# Сначала определяем класс
|
||||||
|
class Foo:
|
||||||
|
def __init__(self, a: A):
|
||||||
|
self.a = a
|
||||||
|
|
||||||
|
@repo.mark_injector()
|
||||||
|
def a_to_foo(a: A) -> Foo:
|
||||||
|
return Foo(a)
|
||||||
|
|
||||||
|
# Warning может быть выведен через warnings.warn
|
||||||
|
@mark_autowired(repo, verbose=True)
|
||||||
|
class Foo2:
|
||||||
|
def __init__(self, a: A):
|
||||||
|
self.a = a
|
||||||
|
|
||||||
|
# Хотя бы один инжектор зарегистрирован
|
||||||
|
assert len(repo.convertor_set) >= 1
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Тесты edge cases
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
class TestMarkAutowiredEdgeCases:
|
||||||
|
"""Тесты краевых случаев в mark_autowired."""
|
||||||
|
|
||||||
|
def test_class_without_init_annotations(self):
|
||||||
|
"""Класс без аннотаций в __init__."""
|
||||||
|
repo = ConvRepo()
|
||||||
|
|
||||||
|
@mark_autowired(repo)
|
||||||
|
class Foo:
|
||||||
|
def __init__(self, a, b): # Нет аннотаций
|
||||||
|
self.a = a
|
||||||
|
self.b = b
|
||||||
|
|
||||||
|
# Конструктор не зарегистрирован (нет аннотаций)
|
||||||
|
assert len(repo.convertor_set) == 0
|
||||||
|
|
||||||
|
def test_class_with_partial_annotations(self):
|
||||||
|
"""Класс с частичными аннотациями."""
|
||||||
|
repo = ConvRepo()
|
||||||
|
|
||||||
|
@mark_autowired(repo)
|
||||||
|
class Foo:
|
||||||
|
def __init__(self, a: A, b): # b без аннотации
|
||||||
|
self.a = a
|
||||||
|
self.b = b
|
||||||
|
|
||||||
|
# Конструктор не зарегистрирован (не все параметры типизированы)
|
||||||
|
assert len(repo.convertor_set) == 0
|
||||||
|
|
||||||
|
def test_method_without_return_annotation(self):
|
||||||
|
"""Метод без аннотации возврата."""
|
||||||
|
repo = ConvRepo()
|
||||||
|
|
||||||
|
@mark_autowired(repo)
|
||||||
|
class Foo:
|
||||||
|
def __init__(self, a: A):
|
||||||
|
self.a = a
|
||||||
|
|
||||||
|
def into_B(self): # Нет return type
|
||||||
|
return B(float(self.a.a))
|
||||||
|
|
||||||
|
# Только конструктор (метод без return type пропущен)
|
||||||
|
assert len(repo.convertor_set) == 1
|
||||||
|
|
||||||
|
def test_multiple_methods(self):
|
||||||
|
"""Класс с несколькими методами."""
|
||||||
|
repo = ConvRepo()
|
||||||
|
|
||||||
|
@mark_autowired(repo)
|
||||||
|
class Foo:
|
||||||
|
def __init__(self, a: A):
|
||||||
|
self.a = a
|
||||||
|
|
||||||
|
def to_B(self) -> B:
|
||||||
|
return B(float(self.a.a))
|
||||||
|
|
||||||
|
def to_C(self) -> C:
|
||||||
|
return C(str(self.a.a))
|
||||||
|
|
||||||
|
# Конструктор + 2 метода
|
||||||
|
assert len(repo.convertor_set) == 3
|
||||||
|
|
||||||
|
def test_nested_class(self):
|
||||||
|
"""Вложенный класс."""
|
||||||
|
repo = ConvRepo()
|
||||||
|
|
||||||
|
class Outer:
|
||||||
|
@mark_autowired(repo)
|
||||||
|
class Inner:
|
||||||
|
def __init__(self, a: A):
|
||||||
|
self.a = a
|
||||||
|
|
||||||
|
# Конструктор вложенного класса зарегистрирован
|
||||||
|
assert len(repo.convertor_set) == 1
|
||||||
|
|
||||||
|
def test_is_basic_type_with_none(self):
|
||||||
|
"""_is_basic_type с None."""
|
||||||
|
from breakshaft.autowire import _is_basic_type
|
||||||
|
|
||||||
|
assert _is_basic_type(None) is True
|
||||||
|
|
||||||
|
def test_is_basic_type_with_basic_types(self):
|
||||||
|
"""_is_basic_type с базовыми типами."""
|
||||||
|
from breakshaft.autowire import _is_basic_type
|
||||||
|
|
||||||
|
assert _is_basic_type(int) is True
|
||||||
|
assert _is_basic_type(str) is True
|
||||||
|
assert _is_basic_type(float) is True
|
||||||
|
assert _is_basic_type(bool) is True
|
||||||
|
|
||||||
|
def test_get_constructor_signature_error(self):
|
||||||
|
"""_get_constructor_signature с ошибкой."""
|
||||||
|
from breakshaft.autowire import _get_constructor_signature
|
||||||
|
|
||||||
|
class BadInit:
|
||||||
|
__init__ = None # type: ignore
|
||||||
|
|
||||||
|
result = _get_constructor_signature(BadInit)
|
||||||
|
assert result is None
|
||||||
|
|
||||||
|
def test_get_method_signature_error(self):
|
||||||
|
"""_get_method_signature с ошибкой."""
|
||||||
|
from breakshaft.autowire import _get_method_signature
|
||||||
|
|
||||||
|
def bad_method():
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Удаляем аннотации чтобы вызвать ошибку
|
||||||
|
bad_method.__annotations__ = None # type: ignore
|
||||||
|
|
||||||
|
result = _get_method_signature(bad_method)
|
||||||
|
assert result is None
|
||||||
|
|
||||||
|
def test_should_register_false(self):
|
||||||
|
"""_should_register возвращает False для дубликата."""
|
||||||
|
from breakshaft.autowire import _should_register
|
||||||
|
|
||||||
|
repo = ConvRepo()
|
||||||
|
|
||||||
|
@repo.mark_injector()
|
||||||
|
def a_to_b(a: A) -> B:
|
||||||
|
return B(float(a.a))
|
||||||
|
|
||||||
|
# Должен вернуть False (уже есть такой инжектор)
|
||||||
|
result = _should_register(repo, (A,), B)
|
||||||
|
assert result is False
|
||||||
|
|
||||||
|
def test_mark_autowired_without_repo(self):
|
||||||
|
"""mark_autowired без repo."""
|
||||||
|
from breakshaft.autowire import mark_autowired
|
||||||
|
|
||||||
|
# Вызов без repo возвращает decorator
|
||||||
|
decorator = mark_autowired(None) # type: ignore
|
||||||
|
assert callable(decorator)
|
||||||
Reference in New Issue
Block a user