Реализована автоматическая регистрация конструктора и методов класса:
- @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>
423 lines
13 KiB
Python
423 lines
13 KiB
Python
"""
|
||
Тесты для 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
|