Skip to content

Commit 6043b5b

Browse files
authored
fix: singleton reinstantiated if registered at last & fix: initialized class attribute being overridden (#44)
* fix: singleton reinstantiated if registered at last changed service provider build logic in order to avoid singletons to be instantiated n times when they're registered after its dependant * fix: initialized class attribute being overridden changed the "ignore attributes" logic so that if a class variable has already been initialized externally, rodi doesn't attempt to reinitialize it (and to also prevent overriding it if the initialized class variable is also a registered object).
1 parent de16370 commit 6043b5b

3 files changed

Lines changed: 121 additions & 11 deletions

File tree

rodi/__init__.py

Lines changed: 26 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -475,7 +475,12 @@ def _get_resolver(self, desired_type, context: ResolutionContext):
475475
assert (
476476
reg is not None
477477
), f"A resolver for type {class_name(desired_type)} is not configured"
478-
return reg(context)
478+
resolver = reg(context)
479+
480+
# add the resolver to the context, so we can find it
481+
# next time we need it
482+
context.resolved[desired_type] = resolver
483+
return resolver
479484

480485
def _get_resolvers_for_parameters(
481486
self,
@@ -567,11 +572,12 @@ def _ignore_class_attribute(self, key: str, value) -> bool:
567572
"""
568573
Returns a value indicating whether a class attribute should be ignored for
569574
dependency resolution, by name and value.
575+
It's ignored if it's a ClassVar or if it's already initialized explicitly.
570576
"""
571-
try:
572-
return value.__origin__ is ClassVar
573-
except AttributeError:
574-
return False
577+
is_classvar = getattr(value, "__origin__", None) is ClassVar
578+
is_initialized = getattr(self.concrete_type, key, None) is not None
579+
580+
return is_classvar or is_initialized
575581

576582
def _resolve_by_annotations(
577583
self, context: ResolutionContext, annotations: Dict[str, Type]
@@ -1146,14 +1152,24 @@ def build_provider(self) -> Services:
11461152
_map: Dict[Union[str, Type], Type] = {}
11471153

11481154
for _type, resolver in self._map.items():
1149-
# NB: do not call resolver if one was already prepared for the type
1150-
assert _type not in context.resolved, "_map keys must be unique"
1151-
11521155
if isinstance(resolver, DynamicResolver):
11531156
context.dynamic_chain.clear()
11541157

1155-
_map[_type] = resolver(context)
1156-
context.resolved[_type] = _map[_type]
1158+
if _type in context.resolved:
1159+
# assert _type not in context.resolved, "_map keys must be unique"
1160+
# check if its in the map
1161+
if _type in _map:
1162+
# NB: do not call resolver if one was already prepared for the
1163+
# type
1164+
raise OverridingServiceException(_type, resolver)
1165+
else:
1166+
resolved = context.resolved[_type]
1167+
else:
1168+
# add to context so that we don't repeat operations
1169+
resolved = resolver(context)
1170+
context.resolved[_type] = resolved
1171+
1172+
_map[_type] = resolved
11571173

11581174
type_name = class_name(_type)
11591175
if "." not in type_name:

tests/test_examples.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,11 @@
1212

1313
@pytest.mark.parametrize("file_path", examples)
1414
def test_example(file_path: str):
15-
module_name = file_path.replace("./examples/", "").replace(".py", "")
15+
module_name = (
16+
# Windows
17+
file_path.replace("./examples\\", "")
18+
# Unix
19+
.replace("./examples/", "").replace(".py", "")
20+
)
1621
# assertions are in imported modules
1722
importlib.import_module(module_name)

tests/test_services.py

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2539,3 +2539,92 @@ class A:
25392539
a = container.resolve(A)
25402540

25412541
assert a.foo == "foo"
2542+
2543+
2544+
def test_singleton_register_order_last():
2545+
"""
2546+
The registration order of singletons should not matter.
2547+
Check that singletons are not registered twice when they are registered
2548+
after their dependents.
2549+
"""
2550+
2551+
class Bar:
2552+
foo: Foo
2553+
2554+
class Bar2:
2555+
foo: Foo
2556+
2557+
container = Container()
2558+
container.register(Bar)
2559+
container.register(Bar2)
2560+
container._add_exact_singleton(Foo)
2561+
2562+
bar = container.resolve(Bar)
2563+
bar2 = container.resolve(Bar2)
2564+
foo = container.resolve(Foo)
2565+
2566+
# check that singletons are always the same instance
2567+
assert bar.foo is bar2.foo is foo
2568+
2569+
2570+
def test_singleton_register_order_first():
2571+
"""
2572+
The registration order of singletons should not matter.
2573+
Check that singletons are not registered twice when they are registered
2574+
before their dependents.
2575+
"""
2576+
2577+
class Bar:
2578+
foo: Foo
2579+
2580+
class Bar2:
2581+
foo: Foo
2582+
2583+
container = Container()
2584+
container._add_exact_singleton(Foo)
2585+
container.register(Bar)
2586+
container.register(Bar2)
2587+
2588+
bar = container.resolve(Bar)
2589+
bar2 = container.resolve(Bar2)
2590+
foo = container.resolve(Foo)
2591+
2592+
# check that singletons are always the same instance
2593+
assert bar.foo is bar2.foo is foo
2594+
2595+
2596+
def test_ignore_class_variable_if_already_initialized():
2597+
"""
2598+
if a class variable is already initialized, it should not be overridden by
2599+
resolving a new instance nor fail if rodi can't resolve it.
2600+
"""
2601+
2602+
foo_instance = Foo()
2603+
2604+
class A:
2605+
foo: Foo = foo_instance
2606+
2607+
class B:
2608+
example: ClassVar[str] = "example"
2609+
dependency: A
2610+
2611+
container = Container()
2612+
2613+
container.register(A)
2614+
container.register(B)
2615+
container._add_exact_singleton(Foo)
2616+
2617+
b = container.resolve(B)
2618+
a = container.resolve(A)
2619+
foo = container.resolve(Foo)
2620+
2621+
assert isinstance(a, A)
2622+
assert isinstance(a.foo, Foo)
2623+
assert foo_instance is a.foo
2624+
2625+
assert isinstance(b, B)
2626+
assert b.example == "example"
2627+
assert b.dependency.foo is foo_instance
2628+
2629+
# check that is not being overridden by resolving a new instance
2630+
assert foo is not a.foo

0 commit comments

Comments
 (0)