Skip to content
This repository has been archived by the owner on Apr 29, 2024. It is now read-only.

Commit

Permalink
fix(di): ensure leafmost singletons are instantiated once
Browse files Browse the repository at this point in the history
  • Loading branch information
Bogdanp committed Oct 15, 2018
1 parent 9031528 commit c79049d
Show file tree
Hide file tree
Showing 3 changed files with 70 additions and 5 deletions.
7 changes: 7 additions & 0 deletions docs/source/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,13 @@ All notable changes to this project will be documented in this file.
`Unreleased`_
-------------

Fixed
^^^^^

* Fixed an issue where Singletons that were depdendencies of other
singletons were instantiated multiple times.


`0.7.0`_ -- 2018-10-15
----------------------

Expand Down
14 changes: 13 additions & 1 deletion molten/dependency_injection.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,12 +88,24 @@ def __init__(self, components: List[Component[Any]], singletons: Optional[Dict[C
self.components = components or []
self.singletons = singletons or {}

def is_singleton(component: Component[Any]) -> bool:
return getattr(component, "is_singleton", False) and component not in self.singletons

for component in components:
if getattr(component, "is_singleton", False) and component not in self.singletons:
if is_singleton(component):
resolver = self.get_resolver()
resolved_component = resolver.resolve(component.resolve)
self.singletons[component] = resolved_component()

# Also register any singleton components that were
# instantiated during the resolving process of the
# current component. This is necessary so that
# singletons don't get instantiated by each dependent
# component in part.
for component, instance in resolver.instances.items():
if is_singleton(component):
self.singletons[component] = instance

def get_resolver(self, instances: Optional[Dict[Any, Any]] = None) -> "DependencyResolver":
"""Get the resolver for this Injector.
"""
Expand Down
54 changes: 50 additions & 4 deletions tests/test_dependency_injection.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from inspect import Parameter
from itertools import permutations
from typing import NewType

import pytest
Expand All @@ -20,22 +21,39 @@ def resolve(self) -> Settings:
return Settings()


class DB:

class Metrics:
__slots__ = ["settings"]

def __init__(self, settings: Settings) -> None:
self.settings = settings


class MetricsComponent:
is_singleton = True

def can_handle_parameter(self, parameter: Parameter) -> bool:
return parameter.annotation is Metrics

def resolve(self, settings: Settings) -> Metrics:
return Metrics(settings)


class DB:
__slots__ = ["settings", "metrics"]

def __init__(self, settings: Settings, metrics: Metrics) -> None:
self.settings = settings
self.metrics = metrics


class DBComponent:
is_singleton = True

def can_handle_parameter(self, parameter: Parameter) -> bool:
return parameter.annotation is DB

def resolve(self, settings: Settings) -> DB:
return DB(settings)
def resolve(self, settings: Settings, metrics: Metrics) -> DB:
return DB(settings, metrics)


class Accounts:
Expand All @@ -58,6 +76,7 @@ def test_di_can_inject_dependencies():
# Given that I have a DI instance
di = DependencyInjector(components=[
SettingsComponent(),
MetricsComponent(),
DBComponent(),
AccountsComponent(),
])
Expand Down Expand Up @@ -105,3 +124,30 @@ def example(a_component: AComponent) -> None:
# Then a DIError should be raised
with pytest.raises(DIError):
resolved_example()


@pytest.mark.parametrize("component_classes", permutations([AccountsComponent, DBComponent, MetricsComponent, SettingsComponent]))
def test_di_correctly_instantiates_singleton_dependencies(component_classes):
# Given that I have a DI instance with its components ordered randomly
di = DependencyInjector(components=[k() for k in component_classes])

# And a function that uses DI
def example(accounts: Accounts, db: DB, metrics: Metrics, settings: Settings):
return accounts, db, metrics, settings

# Then all the singletons should only be instantiated once
resolver = di.get_resolver()
resolved_example = resolver.resolve(example)
accounts_1, db_1, metrics_1, settings_1 = resolved_example()
assert metrics_1.settings is settings_1
assert db_1.settings is settings_1
assert db_1.metrics is metrics_1
assert accounts_1.db is db_1

resolver = di.get_resolver()
resolved_example = resolver.resolve(example)
accounts_2, db_2, metrics_2, settings_2 = resolved_example()
assert accounts_2 is not accounts_1
assert db_2 is db_1
assert metrics_2 is metrics_1
assert settings_2 is settings_1

0 comments on commit c79049d

Please # to comment.