Compare commits
2 Commits
feature/in
...
feature/au
| 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)
|
||||
|
||||
Приоритизация инжекторов:
|
||||
from breakshaft import ConvRepo, more_than, less_than
|
||||
Автовайринг классов:
|
||||
from breakshaft import ConvRepo, mark_autowired
|
||||
|
||||
repo = ConvRepo()
|
||||
|
||||
@repo.mark_injector(priority=10.0) # Абсолютный приоритет
|
||||
def int_to_a_v1(i: int) -> A: ...
|
||||
@mark_autowired(repo)
|
||||
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 int_to_a_v2(i: int) -> A: ...
|
||||
def into_B(self) -> B:
|
||||
return self.b
|
||||
|
||||
# Автоматически зарегистрировано:
|
||||
# - (A, B) -> Foo (конструктор)
|
||||
# - Foo -> B (метод into_B)
|
||||
|
||||
Исключения:
|
||||
from breakshaft import (
|
||||
@@ -30,6 +37,7 @@ breakshaft - библиотека для генерации преобразов
|
||||
AmbiguousPath,
|
||||
MissingReturnType,
|
||||
CircularDependency, # Для циклов в относительных приоритетах
|
||||
NonCommutativeWarning, # Для автовайринга
|
||||
# ... другие исключения
|
||||
)
|
||||
"""
|
||||
@@ -38,6 +46,7 @@ from .convertor import ConvRepo
|
||||
from .graph_walker import GraphWalker
|
||||
from .models import ConversionPoint, Callgraph, CallgraphVariant, TransformationPoint
|
||||
from .priority_types import more_than, less_than, PriorityValue, MoreThan, LessThan
|
||||
from .autowire import mark_autowired, NonCommutativeWarning
|
||||
from .exceptions import (
|
||||
BreakshaftError,
|
||||
BreakshaftRuntimeError,
|
||||
@@ -80,6 +89,9 @@ __all__ = [
|
||||
"PriorityValue",
|
||||
"MoreThan",
|
||||
"LessThan",
|
||||
# Автовайринг
|
||||
"mark_autowired",
|
||||
"NonCommutativeWarning",
|
||||
# Исключения
|
||||
"BreakshaftError",
|
||||
"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