diff --git a/AUTOWIRE_DESIGN.md b/AUTOWIRE_DESIGN.md new file mode 100644 index 0000000..2024132 --- /dev/null +++ b/AUTOWIRE_DESIGN.md @@ -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* +*Статус: Черновик* diff --git a/src/breakshaft/__init__.py b/src/breakshaft/__init__.py index 11059d1..28101db 100644 --- a/src/breakshaft/__init__.py +++ b/src/breakshaft/__init__.py @@ -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", diff --git a/src/breakshaft/autowire.py b/src/breakshaft/autowire.py new file mode 100644 index 0000000..718bc33 --- /dev/null +++ b/src/breakshaft/autowire.py @@ -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', +] diff --git a/tests/test_autowire.py b/tests/test_autowire.py new file mode 100644 index 0000000..cc9a783 --- /dev/null +++ b/tests/test_autowire.py @@ -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