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*
*Статус: Черновик*