Skip to content

Commit 3843966

Browse files
committed
Add support for constructor arguments.
1 parent ef22679 commit 3843966

File tree

15 files changed

+361
-36
lines changed

15 files changed

+361
-36
lines changed

README.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,21 @@ To contribute, create a pull request on the develop branch following the [git fl
139139

140140
## Release Notes
141141

142+
### [1.0.0-alpha.4](https://github.com/runemalm/py-dependency-injection/releases/tag/v1.0.0-alpha.4) (2024-03-02)
143+
144+
- **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.
145+
146+
**Usage Example:**
147+
```python
148+
# Registering a dependency with constructor arguments
149+
dependency_container.register_transient(
150+
SomeInterface, SomeClass,
151+
constructor_args={"arg1": value1, "arg2": value2}
152+
)
153+
```
154+
155+
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.
156+
142157
### [1.0.0-alpha.3](https://github.com/runemalm/py-dependency-injection/releases/tag/v1.0.0-alpha.3) (2024-03-02)
143158

144159
- **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/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.

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
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
from dependency_injection.container import DependencyContainer
2+
from unit_test.unit_test_case import UnitTestCase
3+
4+
5+
class TestRegisterWithArgs(UnitTestCase):
6+
7+
def test_register_with_constructor_args(
8+
self,
9+
):
10+
# arrange
11+
class Vehicle:
12+
pass
13+
14+
class Car(Vehicle):
15+
pass
16+
17+
dependency_container = DependencyContainer.get_instance()
18+
interface = Vehicle
19+
dependency_class = Car
20+
21+
# act + assert (no exception)
22+
dependency_container.register_transient(interface, dependency_class, constructor_args={"color": "red", "mileage": 3800})

tests/unit_test/container/resolve/test_resolve_scoped.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
from dependency_injection.container import DependencyContainer
2-
from unit_test.car import Car
32
from unit_test.unit_test_case import UnitTestCase
4-
from unit_test.vehicle import Vehicle
53

64

75
class TestResolveScoped(UnitTestCase):
@@ -10,6 +8,12 @@ def test_resolve_scoped_in_same_scope_returns_same_instance(
108
self,
119
):
1210
# arrange
11+
class Vehicle:
12+
pass
13+
14+
class Car(Vehicle):
15+
pass
16+
1317
dependency_container = DependencyContainer.get_instance()
1418
interface = Vehicle
1519
dependency_class = Car
@@ -26,6 +30,12 @@ def test_resolve_scoped_in_different_scopes_returns_different_instances(
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

0 commit comments

Comments
 (0)