feat: автовайринг классов (@mark_autowired)

Реализована автоматическая регистрация конструктора и методов класса:

- @mark_autowired(repo) декоратор
- register_init=True: регистрировать конструктор (A,B)->Foo
- register_methods=True: регистрировать методы Foo->B
- skip_basic_types=True: пропускать базовые типы (int, str)
- priority: приоритет инжекторов
- verbose: предупреждения о дубликатах

Файлы:
- autowire.py: mark_autowired(), NonCommutativeWarning
- test_autowire.py: 19 тестов
- __init__.py: экспорт mark_autowired, NonCommutativeWarning
- AUTOWIRE_DESIGN.md: документация

Пример:
    @mark_autowired(repo)
    class Foo:
        def __init__(self, a: A, b: B): ...
        def into_B(self) -> B: ...

    # Зарегистрировано: (A,B)->Foo, Foo->B

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
This commit is contained in:
Qwen Code Assistant
2026-03-28 18:47:54 +00:00
parent 4a7fb58b78
commit fe0e7dfd27
4 changed files with 1030 additions and 6 deletions

299
AUTOWIRE_DESIGN.md Normal file
View 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*
*Статус: Черновик*

View File

@@ -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
View 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',
]

422
tests/test_autowire.py Normal file
View File

@@ -0,0 +1,422 @@
"""
Тесты для 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