-
Notifications
You must be signed in to change notification settings - Fork 10
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add option to override services (#13)
- Loading branch information
Showing
9 changed files
with
234 additions
and
12 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
::: wireup.ioc.override_manager.OverrideManager |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
While wireup tries to make it as easy as possible to test services by not modifying | ||
the underlying classes in any way even when decorated, sometimes you need to be able | ||
to swap a service object on the fly for a different one such as a mock. | ||
|
||
This process can be useful in testing autowired targets for which there is no easy | ||
way to pass a mock object. | ||
|
||
The `container.override` property provides access to a number of useful methods | ||
which will help temporarily overriding dependencies | ||
(See [override manager](class/override_manager.md)). | ||
|
||
|
||
!!! info "Good to know" | ||
* Overriding only applies to future autowire calls. | ||
* If a singleton service A has been initialized, it is not possible to override any | ||
of its dependencies as the object is already in memory. You may need to override | ||
Service A directly instead of any transient dependencies. | ||
* When using interfaces override the interface rather than any of its implementations. | ||
|
||
## Example | ||
|
||
```python | ||
random_mock = MagicMock() | ||
random_mock.get_random.return_value = 5 | ||
|
||
with self.container.override.service(target=RandomService, new=random_mock): | ||
# Assuming in the context of a web app: | ||
# /random endpoint has a dependency on RandomService | ||
# any requests to inject RandomService during the lifetime | ||
# of this context manager will result in random_mock being injected instead. | ||
response = client.get("/random") | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,97 @@ | ||
import unittest | ||
|
||
from test.fixtures import FooBase, FooBar | ||
from test.services.no_annotations.random.random_service import RandomService | ||
from unittest.mock import MagicMock, patch | ||
|
||
from typing_extensions import Annotated | ||
from wireup import DependencyContainer, ParameterBag, Wire | ||
from wireup.ioc.override_manager import OverrideManager | ||
from wireup.ioc.types import ServiceOverride | ||
|
||
|
||
class TestContainerOverride(unittest.TestCase): | ||
def setUp(self) -> None: | ||
self.container = DependencyContainer(ParameterBag()) | ||
|
||
def test_container_overrides_deps_service_locator(self): | ||
self.container.register(RandomService) | ||
|
||
random_mock = MagicMock() | ||
random_mock.get_random.return_value = 5 | ||
|
||
with self.container.override.service(target=RandomService, new=random_mock): | ||
svc = self.container.get(RandomService) | ||
self.assertEqual(svc.get_random(), 5) | ||
|
||
random_mock.get_random.assert_called_once() | ||
self.assertEqual(self.container.get(RandomService).get_random(), 4) | ||
|
||
def test_container_overrides_deps_service_locator_interface(self): | ||
self.container.abstract(FooBase) | ||
|
||
foo_mock = MagicMock() | ||
|
||
with patch.object(foo_mock, "foo", new="mock"): | ||
with self.container.override.service(target=FooBase, new=foo_mock): | ||
svc = self.container.get(FooBase) | ||
self.assertEqual(svc.foo, "mock") | ||
|
||
def test_container_override_many_with_qualifier(self): | ||
self.container.register(RandomService, qualifier="Rand1") | ||
self.container.register(RandomService, qualifier="Rand2") | ||
|
||
@self.container.autowire | ||
def target( | ||
rand1: Annotated[RandomService, Wire(qualifier="Rand1")], | ||
rand2: Annotated[RandomService, Wire(qualifier="Rand2")], | ||
): | ||
self.assertEqual(rand1.get_random(), 5) | ||
self.assertEqual(rand2.get_random(), 6) | ||
|
||
self.assertIsInstance(rand1, MagicMock) | ||
self.assertIsInstance(rand2, MagicMock) | ||
|
||
rand1_mock = MagicMock() | ||
rand1_mock.get_random.return_value = 5 | ||
|
||
rand2_mock = MagicMock() | ||
rand2_mock.get_random.return_value = 6 | ||
|
||
overrides = [ | ||
ServiceOverride(target=RandomService, qualifier="Rand1", new=rand1_mock), | ||
ServiceOverride(target=RandomService, qualifier="Rand2", new=rand2_mock), | ||
] | ||
with self.container.override.services(overrides=overrides): | ||
target() | ||
|
||
rand1_mock.get_random.assert_called_once() | ||
rand2_mock.get_random.assert_called_once() | ||
|
||
def test_container_override_with_interface(self): | ||
self.container.abstract(FooBase) | ||
self.container.register(FooBar) | ||
|
||
@self.container.autowire | ||
def target(foo: FooBase): | ||
self.assertEqual(foo.foo, "mock") | ||
self.assertIsInstance(foo, MagicMock) | ||
|
||
foo_mock = MagicMock() | ||
|
||
with patch.object(foo_mock, "foo", new="mock"): | ||
with self.container.override.service(target=FooBase, new=foo_mock): | ||
svc = self.container.get(FooBase) | ||
self.assertEqual(svc.foo, "mock") | ||
|
||
target() | ||
|
||
def test_clear_services_removes_all(self): | ||
overrides = {} | ||
mock1 = MagicMock() | ||
override_mgr = OverrideManager(overrides) | ||
override_mgr.set(RandomService, new=mock1) | ||
self.assertEqual(overrides, {(RandomService, None): mock1}) | ||
|
||
override_mgr.clear() | ||
self.assertEqual(overrides, {}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,63 @@ | ||
from __future__ import annotations | ||
|
||
from contextlib import contextmanager | ||
from typing import TYPE_CHECKING, Any, Iterator | ||
|
||
if TYPE_CHECKING: | ||
from wireup.ioc.types import ContainerProxyQualifierValue, ServiceOverride | ||
|
||
|
||
class OverrideManager: | ||
"""Enables overriding of services registered with the container.""" | ||
|
||
def __init__(self, active_overrides: dict[tuple[type, ContainerProxyQualifierValue], Any]) -> None: | ||
self.__active_overrides = active_overrides | ||
|
||
def set(self, target: type, new: Any, qualifier: ContainerProxyQualifierValue = None) -> None: | ||
"""Override the `target` service with `new`. | ||
Subsequent autowire calls to `target` will result in `new` being injected. | ||
:param target: The target service to override. | ||
:param qualifier: The qualifier of the service to override. Set this if service is registered | ||
with the qualifier parameter set to a value. | ||
:param new: The new object to be injected instead of `target`. | ||
""" | ||
self.__active_overrides[target, qualifier] = new | ||
|
||
def delete(self, target: type, qualifier: ContainerProxyQualifierValue = None) -> None: | ||
"""Clear active override for the `target` service.""" | ||
if (target, qualifier) in self.__active_overrides: | ||
del self.__active_overrides[target, qualifier] | ||
|
||
def clear(self) -> None: | ||
"""Clear active service overrides.""" | ||
self.__active_overrides.clear() | ||
|
||
@contextmanager | ||
def service(self, target: type, new: Any, qualifier: ContainerProxyQualifierValue = None) -> Iterator[None]: | ||
"""Override the `target` service with `new` for the duration of the context manager. | ||
Subsequent autowire calls to `target` will result in `new` being injected. | ||
:param target: The target service to override. | ||
:param qualifier: The qualifier of the service to override. Set this if service is registered | ||
with the qualifier parameter set to a value. | ||
:param new: The new object to be injected instead of `target`. | ||
""" | ||
try: | ||
self.set(target, new, qualifier) | ||
yield | ||
finally: | ||
self.delete(target, qualifier) | ||
|
||
@contextmanager | ||
def services(self, overrides: list[ServiceOverride]) -> Iterator[None]: | ||
"""Override a number of services with new for the duration of the context manager.""" | ||
try: | ||
for override in overrides: | ||
self.set(override.target, override.new, override.qualifier) | ||
yield | ||
finally: | ||
for override in overrides: | ||
self.delete(override.target, override.qualifier) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters