Skip to content

Commit 33ae0b8

Browse files
authored
Merge pull request #2 from runemalm/feature/constructor-args
Feature/constructor args
2 parents ef22679 + 0f4a6b2 commit 33ae0b8

17 files changed

+370
-38
lines changed

README.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,13 @@ third_container = DependencyContainer.get_instance(name="third_container")
6060
dependency_container.register_transient(SomeInterface, SomeClass)
6161
dependency_container.register_scoped(AnotherInterface, AnotherClass)
6262
dependency_container.register_singleton(ThirdInterface, ThirdClass)
63+
64+
# Registering dependencies with constructor arguments
65+
dependency_container.register_transient(
66+
SomeInterface,
67+
SomeClass,
68+
constructor_args={"arg1": value1, "arg2": value2}
69+
)
6370
```
6471

6572
### Resolving dependencies using the container
@@ -139,6 +146,21 @@ To contribute, create a pull request on the develop branch following the [git fl
139146

140147
## Release Notes
141148

149+
### [1.0.0-alpha.4](https://github.com/runemalm/py-dependency-injection/releases/tag/v1.0.0-alpha.4) (2024-03-02)
150+
151+
- **New Feature**: Support for constructor arguments in dependency registration: In this release, we introduce the ability to specify constructor arguments when registering dependencies with the container. This feature provides more flexibility when configuring dependencies, allowing users to customize the instantiation of classes during registration.
152+
153+
**Usage Example:**
154+
```python
155+
# Registering a dependency with constructor arguments
156+
dependency_container.register_transient(
157+
SomeInterface, SomeClass,
158+
constructor_args={"arg1": value1, "arg2": value2}
159+
)
160+
```
161+
162+
Users can now pass specific arguments to be used during the instantiation of the dependency. This is particularly useful when a class requires dynamic or configuration-dependent parameters.
163+
142164
### [1.0.0-alpha.3](https://github.com/runemalm/py-dependency-injection/releases/tag/v1.0.0-alpha.3) (2024-03-02)
143165

144166
- **Breaking Change**: Restriction on `@inject` Decorator: Starting from this version, the `@inject` decorator can now only be used on static class methods and class methods. This change is introduced due to potential pitfalls associated with resolving and injecting dependencies directly into class instance methods using the dependency container.

docs/conf.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434
version = '1.0'
3535

3636
# The full version, including alpha/beta/rc tags
37-
release = '1.0.0-alpha.3'
37+
release = '1.0.0-alpha.4'
3838

3939

4040
# -- General configuration ---------------------------------------------------

docs/versionhistory.rst

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,22 @@
22
Version history
33
###############
44

5+
**1.0.0-alpha.4 (2024-03-02)**
6+
7+
- **New Feature**: Support for constructor arguments in dependency registration: In this release, we introduce the ability to specify constructor arguments when registering dependencies with the container. This feature provides more flexibility when configuring dependencies, allowing users to customize the instantiation of classes during registration.
8+
9+
**Usage Example:**::
10+
11+
# Registering a dependency with constructor arguments
12+
dependency_container.register_transient(
13+
SomeInterface, SomeClass,
14+
constructor_args={"arg1": value1, "arg2": value2}
15+
)
16+
17+
Users can now pass specific arguments to be used during the instantiation of the dependency. This is particularly useful when a class requires dynamic or configuration-dependent parameters.
18+
19+
`View release on GitHub <https://github.com/runemalm/py-dependency-injection/releases/tag/v1.0.0-alpha.4>`_
20+
521
**1.0.0-alpha.3 (2024-03-02)**
622

723
- **Breaking Change**: Restriction on `@inject` Decorator: Starting from this version, the `@inject` decorator can now only be used on static class methods and class methods. This change is introduced due to potential pitfalls associated with resolving and injecting dependencies directly into class instance methods using the dependency container.

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
setup(
88
name='py-dependency-injection',
9-
version='1.0.0-alpha.3',
9+
version='1.0.0-alpha.4',
1010
author='David Runemalm, 2024',
1111
author_email='david.runemalm@gmail.com',
1212
description=

src/dependency_injection/container.py

Lines changed: 54 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import inspect
2+
from typing import Any, Dict, Type
23

34
from dependency_injection.registration import Registration
45
from dependency_injection.scope import DEFAULT_SCOPE_NAME, Scope
@@ -25,20 +26,20 @@ def get_instance(cls, name=None):
2526

2627
return cls._instances[(cls, name)]
2728

28-
def register_transient(self, interface, class_):
29+
def register_transient(self, interface, class_, constructor_args=None):
2930
if interface in self._registrations:
3031
raise ValueError(f"Dependency {interface} is already registered.")
31-
self._registrations[interface] = Registration(interface, class_, Scope.TRANSIENT)
32+
self._registrations[interface] = Registration(interface, class_, Scope.TRANSIENT, constructor_args)
3233

33-
def register_scoped(self, interface, class_):
34+
def register_scoped(self, interface, class_, constructor_args=None):
3435
if interface in self._registrations:
3536
raise ValueError(f"Dependency {interface} is already registered.")
36-
self._registrations[interface] = Registration(interface, class_, Scope.SCOPED)
37+
self._registrations[interface] = Registration(interface, class_, Scope.SCOPED, constructor_args)
3738

38-
def register_singleton(self, interface, class_):
39+
def register_singleton(self, interface, class_, constructor_args=None):
3940
if interface in self._registrations:
4041
raise ValueError(f"Dependency {interface} is already registered.")
41-
self._registrations[interface] = Registration(interface, class_, Scope.SINGLETON)
42+
self._registrations[interface] = Registration(interface, class_, Scope.SINGLETON, constructor_args)
4243

4344
def resolve(self, interface, scope_name=DEFAULT_SCOPE_NAME):
4445
if scope_name not in self._scoped_instances:
@@ -50,24 +51,62 @@ def resolve(self, interface, scope_name=DEFAULT_SCOPE_NAME):
5051
registration = self._registrations[interface]
5152
dependency_scope = registration.scope
5253
dependency_class = registration.class_
54+
constructor_args = registration.constructor_args
55+
56+
self._validate_constructor_args(constructor_args=constructor_args, class_=dependency_class)
5357

5458
if dependency_scope == Scope.TRANSIENT:
55-
return self._inject_dependencies(dependency_class)
59+
return self._inject_dependencies(
60+
class_=dependency_class,
61+
constructor_args=constructor_args
62+
)
5663
elif dependency_scope == Scope.SCOPED:
5764
if interface not in self._scoped_instances[scope_name]:
5865
self._scoped_instances[scope_name][interface] = (
5966
self._inject_dependencies(
6067
class_=dependency_class,
61-
scope_name=scope_name,))
68+
scope_name=scope_name,
69+
constructor_args=constructor_args,
70+
))
6271
return self._scoped_instances[scope_name][interface]
6372
elif dependency_scope == Scope.SINGLETON:
6473
if interface not in self._singleton_instances:
65-
self._singleton_instances[interface] = self._inject_dependencies(dependency_class)
74+
self._singleton_instances[interface] = (
75+
self._inject_dependencies(
76+
class_=dependency_class,
77+
constructor_args=constructor_args
78+
)
79+
)
6680
return self._singleton_instances[interface]
6781

6882
raise ValueError(f"Invalid dependency scope: {dependency_scope}")
6983

70-
def _inject_dependencies(self, class_, scope_name=None):
84+
def _validate_constructor_args(self, constructor_args: Dict[str, Any], class_: Type) -> None:
85+
class_constructor = inspect.signature(class_.__init__).parameters
86+
87+
# Check if any required parameter is missing
88+
missing_params = [param for param in class_constructor.keys() if
89+
param not in ["self", "cls", "args", "kwargs"] and
90+
param not in constructor_args]
91+
if missing_params:
92+
raise ValueError(
93+
f"Missing required constructor arguments: "
94+
f"{', '.join(missing_params)} for class '{class_.__name__}'.")
95+
96+
for arg_name, arg_value in constructor_args.items():
97+
if arg_name not in class_constructor:
98+
raise ValueError(
99+
f"Invalid constructor argument '{arg_name}' for class '{class_.__name__}'. "
100+
f"The class does not have a constructor parameter with this name.")
101+
102+
expected_type = class_constructor[arg_name].annotation
103+
if expected_type != inspect.Parameter.empty:
104+
if not isinstance(arg_value, expected_type):
105+
raise TypeError(
106+
f"Constructor argument '{arg_name}' has an incompatible type. "
107+
f"Expected type: {expected_type}, provided type: {type(arg_value)}.")
108+
109+
def _inject_dependencies(self, class_, scope_name=None, constructor_args=None):
71110
constructor = inspect.signature(class_.__init__)
72111
params = constructor.parameters
73112

@@ -82,6 +121,10 @@ def _inject_dependencies(self, class_, scope_name=None):
82121
# **kwargs parameter
83122
pass
84123
else:
85-
dependencies[param_name] = self.resolve(param_info.annotation, scope_name=scope_name)
124+
# Check if constructor_args has an argument with the same name
125+
if constructor_args and param_name in constructor_args:
126+
dependencies[param_name] = constructor_args[param_name]
127+
else:
128+
dependencies[param_name] = self.resolve(param_info.annotation, scope_name=scope_name)
86129

87130
return class_(**dependencies)
Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
1+
from typing import Any, Dict
2+
13
from dependency_injection.scope import Scope
24

35

46
class Registration():
57

6-
def __init__(self, interface, class_, scope: Scope):
8+
def __init__(self, interface, class_, scope: Scope, constructor_args: Dict[str, Any] = None):
79
self.interface = interface
810
self.class_ = class_
911
self.scope = scope
12+
self.constructor_args = constructor_args or {}

tests/unit_test/car.py

Lines changed: 0 additions & 5 deletions
This file was deleted.

tests/unit_test/container/register/test_register_scoped.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
11
import pytest
22

33
from dependency_injection.container import DependencyContainer
4-
from unit_test.car import Car
54
from unit_test.unit_test_case import UnitTestCase
6-
from unit_test.vehicle import Vehicle
75

86

97
class TestRegisterScoped(UnitTestCase):
@@ -12,6 +10,12 @@ def test_register_scoped_succeeds_when_not_previously_registered(
1210
self,
1311
):
1412
# arrange
13+
class Vehicle:
14+
pass
15+
16+
class Car(Vehicle):
17+
pass
18+
1519
dependency_container = DependencyContainer.get_instance()
1620
interface = Vehicle
1721
dependency_class = Car
@@ -26,6 +30,12 @@ def test_register_scoped_fails_when_already_registered(
2630
self,
2731
):
2832
# arrange
33+
class Vehicle:
34+
pass
35+
36+
class Car(Vehicle):
37+
pass
38+
2939
dependency_container = DependencyContainer.get_instance()
3040
interface = Vehicle
3141
dependency_class = Car

tests/unit_test/container/register/test_register_singleton.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
11
import pytest
22

33
from dependency_injection.container import DependencyContainer
4-
from unit_test.car import Car
54
from unit_test.unit_test_case import UnitTestCase
6-
from unit_test.vehicle import Vehicle
75

86

97
class TestRegisterSingleton(UnitTestCase):
@@ -12,6 +10,12 @@ def test_register_singleton_succeeds_when_not_previously_registered(
1210
self,
1311
):
1412
# arrange
13+
class Vehicle:
14+
pass
15+
16+
class Car(Vehicle):
17+
pass
18+
1519
dependency_container = DependencyContainer.get_instance()
1620
interface = Vehicle
1721
dependency_class = Car
@@ -26,6 +30,12 @@ def test_register_singleton_fails_when_already_registered(
2630
self,
2731
):
2832
# arrange
33+
class Vehicle:
34+
pass
35+
36+
class Car(Vehicle):
37+
pass
38+
2939
dependency_container = DependencyContainer.get_instance()
3040
interface = Vehicle
3141
dependency_class = Car

tests/unit_test/container/register/test_register_transient.py

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
11
import pytest
22

33
from dependency_injection.container import DependencyContainer
4-
from unit_test.car import Car
54
from unit_test.unit_test_case import UnitTestCase
6-
from unit_test.vehicle import Vehicle
75

86

97
class TestRegisterTransient(UnitTestCase):
@@ -12,20 +10,31 @@ def test_register_transient_succeeds_when_not_previously_registered(
1210
self,
1311
):
1412
# arrange
13+
class Vehicle:
14+
pass
15+
16+
class Car(Vehicle):
17+
pass
18+
1519
dependency_container = DependencyContainer.get_instance()
1620
interface = Vehicle
1721
dependency_class = Car
1822

1923
# act
2024
dependency_container.register_transient(interface, dependency_class)
2125

22-
# assert
23-
# (no exception thrown)
26+
# assert (no exception thrown)
2427

2528
def test_register_transient_fails_when_already_registered(
2629
self,
2730
):
2831
# arrange
32+
class Vehicle:
33+
pass
34+
35+
class Car(Vehicle):
36+
pass
37+
2938
dependency_container = DependencyContainer.get_instance()
3039
interface = Vehicle
3140
dependency_class = Car

0 commit comments

Comments
 (0)