Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 14 additions & 24 deletions conftest.py
Original file line number Diff line number Diff line change
@@ -1,36 +1,26 @@
import logging
from collections.abc import Iterator
from unittest.mock import patch

import pytest

from injection import Module, mod
from injection._core.module import Module as CoreModule
from injection._core.module import Module
from injection.loaders import PythonModuleLoader
from tests.helpers import EventHistory
from tests.helpers.event_history import EventHistory

logging.basicConfig(level=logging.DEBUG)


@pytest.fixture(scope="session", autouse=True)
def __patch_sys_modules() -> Iterator[None]:
with patch.object(PythonModuleLoader, "_sys_modules", {}):
yield


@pytest.fixture(scope="function", autouse=True)
def unlock():
yield
mod().unlock()
@pytest.fixture
def event_history(module: Module) -> EventHistory:
history = EventHistory()
module.add_listener(history)
return history


@pytest.fixture(scope="function")
def module() -> Module | CoreModule:
return CoreModule()
@pytest.fixture
def module() -> Module:
return Module()


@pytest.fixture(scope="function")
def event_history(module) -> EventHistory:
history = EventHistory()
module.add_listener(history)
return history
@pytest.fixture(autouse=True, scope="session")
def patch_sys_modules() -> Iterator[None]:
with patch.object(PythonModuleLoader, "_sys_modules", {}):
yield
File renamed without changes.
File renamed without changes.
34 changes: 34 additions & 0 deletions tests/_core/common/test_event.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
from contextlib import AbstractContextManager

from injection._core.common.event import Event, EventChannel, EventListener
from tests.helpers.event_history import EventHistory


class _Event(Event): ...


class _SilentListener(EventListener):
def on_event(self, event: Event, /) -> AbstractContextManager[None] | None:
return None


class TestEventChannel:
def test_dispatch_with_listener_without_context_manager(self) -> None:
history = EventHistory()
silent = _SilentListener()
channel = EventChannel().add_listener(silent).add_listener(history)
event = _Event()

with channel.dispatch(event):
pass

assert history.events == (event,)

def test_dispatch_with_removed_listener(self) -> None:
history = EventHistory()
channel = EventChannel().add_listener(history).remove_listener(history)

with channel.dispatch(_Event()):
pass

assert history.events == ()
36 changes: 36 additions & 0 deletions tests/_core/common/test_type.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
from collections.abc import AsyncIterator, Callable, Iterator
from typing import Any

import pytest

from injection._core.common.type import get_yield_types


class A: ...


def _iterator() -> Iterator[A]:
raise NotImplementedError


async def _async_iterator() -> AsyncIterator[A]:
raise NotImplementedError


def _function() -> A:
raise NotImplementedError


@pytest.mark.parametrize(
("function", "expected"),
[
(_iterator, (A,)),
(_async_iterator, (A,)),
(_function, ()),
],
)
def test_get_yield_types_with_return_type_returns_yield_types(
expected: tuple[Any, ...],
function: Callable[..., Any],
) -> None:
assert get_yield_types(function) == expected
41 changes: 41 additions & 0 deletions tests/_core/test_asfunction.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
from typing import NamedTuple

from injection._core.asfunction import asfunction
from injection._core.module import Module


class A: ...


def test_asfunction_with_sync_call_method_returns_function(module: Module) -> None:
module.injectable(A)

@asfunction(module=module)
class GetA(NamedTuple):
a: A

def __call__(self, suffix: str) -> tuple[A, str]:
return self.a, suffix

a, suffix = GetA("suffix")

assert isinstance(a, A)
assert suffix == "suffix"
assert GetA.__name__ == "GetA"


async def test_asfunction_with_async_call_method_returns_coroutine_function(
module: Module,
) -> None:
@module.injectable
async def recipe() -> A:
return A()

@asfunction(module=module)
class GetA(NamedTuple):
a: A

async def __call__(self) -> A:
return self.a

assert isinstance(await GetA(), A)
88 changes: 88 additions & 0 deletions tests/_core/test_descriptors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
from dataclasses import dataclass
from typing import ClassVar

import pytest

from injection._core.descriptors import LazyInstance, MappedScope
from injection._core.module import Module


class A: ...


class B: ...


class TestMappedScope:
def test_get_with_class_returns_descriptor(self) -> None:
descriptor = MappedScope("request")

class Bindings:
scope = descriptor

assert Bindings.scope is descriptor

def test_set_name_with_second_owner_raises_type_error(self) -> None:
class First:
scope = MappedScope("request")

with pytest.raises(TypeError):

class Second:
scope = First.scope


class TestBoundMappedScope:
def test_define_with_values(self, module: Module) -> None:
@dataclass
class Bindings:
a: A
b: B | None = None

scope: ClassVar[MappedScope] = MappedScope("request", module)

a = A()

with Bindings(a).scope.define():
assert module.find_instance(A) is a
assert module.get_instance(B) is NotImplemented

assert module.get_instance(A) is NotImplemented

async def test_adefine_with_values(self, module: Module) -> None:
@dataclass
class Bindings:
a: A

scope: ClassVar[MappedScope] = MappedScope("request", module)

a = A()

async with Bindings(a).scope.adefine():
assert module.find_instance(A) is a

assert module.get_instance(A) is NotImplemented


class TestLazyInstance:
def test_get_with_instance_returns_instance(self, module: Module) -> None:
module.injectable(A)

class Service:
a = LazyInstance(A, module=module)

assert isinstance(Service().a, A)

def test_get_with_class_returns_descriptor(self, module: Module) -> None:
descriptor = LazyInstance(A, module=module)

class Service:
a = descriptor

assert Service.a is descriptor

def test_get_without_injectable_returns_default(self, module: Module) -> None:
class Service:
a = LazyInstance(A, None, module=module)

assert Service().a is None
108 changes: 108 additions & 0 deletions tests/_core/test_locator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
from typing import Any

import pytest

from injection._core.injectables import ConstantInjectable
from injection._core.locator import (
Locator,
LocatorDependenciesUpdated,
Mode,
StaticInjectableBroker,
Updater,
)
from injection._core.module import Module
from injection.exceptions import NoInjectable
from tests.helpers.event_history import EventHistory


class A: ...


class B: ...


def _updater(instance: A, mode: Mode) -> Updater[A]:
return Updater(
classes=(A,),
broker=StaticInjectableBroker(ConstantInjectable(instance)),
mode=mode,
)


class TestLocator:
def test_request_without_record_raises_no_injectable(
self,
module: Module,
) -> None:
with pytest.raises(NoInjectable):
Locator().request(A, module)

@pytest.mark.parametrize(
("existing", "new"),
[
(Mode.FALLBACK, Mode.NORMAL),
(Mode.FALLBACK, Mode.OVERRIDE),
(Mode.NORMAL, Mode.OVERRIDE),
(Mode.OVERRIDE, Mode.OVERRIDE),
],
)
def test_update_with_stronger_mode_replaces_record(
self,
existing: Mode,
module: Module,
new: Mode,
) -> None:
locator = Locator().update(_updater(A(), existing))
instance = A()

locator.update(_updater(instance, new))

assert locator.request(A, module).get_instance() is instance

@pytest.mark.parametrize(
("existing", "new"),
[
(Mode.NORMAL, Mode.FALLBACK),
(Mode.OVERRIDE, Mode.FALLBACK),
(Mode.OVERRIDE, Mode.NORMAL),
],
)
def test_update_with_weaker_mode_keeps_record(
self,
existing: Mode,
module: Module,
new: Mode,
) -> None:
instance = A()
locator = Locator().update(_updater(instance, existing))
history = EventHistory()
locator.add_listener(history)

locator.update(_updater(A(), new))

assert locator.request(A, module).get_instance() is instance
assert history.events == ()

@pytest.mark.parametrize("mode", [Mode.FALLBACK, Mode.NORMAL])
def test_update_with_same_mode_raises_runtime_error(self, mode: Mode) -> None:
locator = Locator().update(_updater(A(), mode))

with pytest.raises(RuntimeError):
locator.update(_updater(A(), mode))


class TestLocatorDependenciesUpdated:
@pytest.mark.parametrize(
("classes", "expected"),
[
((), "0 dependency have been updated."),
((A,), f"1 dependency have been updated: `{A}`."),
((A, B), f"2 dependencies have been updated: `{A}`, `{B}`."),
],
)
def test_str_with_updated_classes_returns_message(
self, classes: tuple[Any, ...], expected: str
) -> None:
event = LocatorDependenciesUpdated(Locator(), classes, Mode.NORMAL)

assert str(event) == expected
Loading
Loading