Files
breakshaft/tests/test_autowire.py
Qwen Code Assistant fe0e7dfd27 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>
2026-03-28 18:47:54 +00:00

423 lines
13 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
Тесты для 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