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

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