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:
422
tests/test_autowire.py
Normal file
422
tests/test_autowire.py
Normal 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
|
||||
Reference in New Issue
Block a user