Skip to content

Commit 1c97bb8

Browse files
authored
Merge pull request #4 from runemalm/feature/next-version
Add next version.
2 parents e84eddd + 37c5bec commit 1c97bb8

22 files changed

+749
-166
lines changed

README.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,23 @@ dependency_container.register_transient(
6767
SomeClass,
6868
constructor_args={"arg1": value1, "arg2": value2}
6969
)
70+
71+
# Registering dependencies with a factory
72+
dependency_container.register_factory(
73+
SomeInterface,
74+
lambda: SomeClass(arg1=value1, arg2=value2)
75+
)
76+
77+
# Registering dependencies with an instance
78+
instance = SomeClass(arg1=value1, arg2=value2)
79+
dependency_container.register_instance(SomeInterface, instance)
80+
81+
# Registering dependencies with tags
82+
dependency_container.register_transient(
83+
SomeInterface,
84+
SomeClass,
85+
tags={SomeAdjective, AnotherAdjective}
86+
)
7087
```
7188

7289
### Resolving dependencies using the container
@@ -80,6 +97,9 @@ scoped_instance = dependency_container.resolve(AnotherInterface, scope_name="som
8097

8198
# Resolve singleton instance (consistent across the entire application).
8299
singleton_instance = dependency_container.resolve(ThirdInterface)
100+
101+
# Resolve all instances with a specific tag
102+
tagged_instances = dependency_container.resolve_all(tags={SomeAdjective})
83103
```
84104

85105
### Constructor injection
@@ -146,6 +166,12 @@ To contribute, create a pull request on the develop branch following the [git fl
146166

147167
## Release Notes
148168

169+
### [1.0.0-alpha.6](https://github.com/runemalm/py-dependency-injection/releases/tag/v1.0.0-alpha.6) (2024-03-23)
170+
171+
- **Factory Registration:** Added support for registering dependencies using factory functions for dynamic instantiation.
172+
- **Instance Registration:** Enabled registering existing instances as dependencies.
173+
- **Tag-based Registration and Resolution:** Introduced the ability to register and resolve dependencies using tags for flexible dependency management.
174+
149175
### [1.0.0-alpha.5](https://github.com/runemalm/py-dependency-injection/releases/tag/v1.0.0-alpha.5) (2024-03-03)
150176

151177
- **Critical Package Integrity Fix**: This release addresses a critical issue that affected the packaging of the Python library in all previous alpha releases (1.0.0-alpha.1 to 1.0.0-alpha.4). The problem involved missing source files in the distribution, rendering the library incomplete and non-functional.

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.4'
37+
release = '1.0.0-alpha.6'
3838

3939

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

docs/versionhistory.rst

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

5+
**1.0.0-alpha.6 (2024-03-23)**
6+
7+
- **Factory Registration:** Added support for registering dependencies using factory functions for dynamic instantiation.
8+
9+
- **Instance Registration:** Enabled registering existing instances as dependencies.
10+
11+
- **Tag-based Registration and Resolution:** Introduced the ability to register and resolve dependencies using tags for flexible dependency management.
12+
13+
`View release on GitHub <https://github.com/runemalm/py-dependency-injection/releases/tag/v1.0.0-alpha.6>`_
14+
515
**1.0.0-alpha.5 (2024-03-03)**
616

717
- **Critical Package Integrity Fix**: This release addresses a critical issue that affected the packaging of the Python library in all previous alpha releases (1.0.0-alpha.1 to 1.0.0-alpha.4). The problem involved missing source files in the distribution, rendering the library incomplete and non-functional.

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.5',
9+
version='1.0.0-alpha.6',
1010
author='David Runemalm, 2024',
1111
author_email='david.runemalm@gmail.com',
1212
description=

src/dependency_injection/container.py

Lines changed: 83 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,26 @@
11
import inspect
2-
from typing import Any, Dict, Type
2+
3+
from typing import Any, Callable, Dict, List, Optional, TypeVar, Type
34

45
from dependency_injection.registration import Registration
56
from dependency_injection.scope import DEFAULT_SCOPE_NAME, Scope
67
from dependency_injection.utils.singleton_meta import SingletonMeta
78

9+
Self = TypeVar('Self', bound='DependencyContainer')
10+
811

912
DEFAULT_CONTAINER_NAME = "default_container"
1013

1114
class DependencyContainer(metaclass=SingletonMeta):
1215

13-
def __init__(self, name=None):
16+
def __init__(self, name: str=None):
1417
self.name = name if name is not None else DEFAULT_CONTAINER_NAME
1518
self._registrations = {}
1619
self._singleton_instances = {}
1720
self._scoped_instances = {}
1821

1922
@classmethod
20-
def get_instance(cls, name=None):
23+
def get_instance(cls, name: str=None) -> Self:
2124
if name is None:
2225
name = DEFAULT_CONTAINER_NAME
2326

@@ -26,88 +29,118 @@ def get_instance(cls, name=None):
2629

2730
return cls._instances[(cls, name)]
2831

29-
def register_transient(self, interface, class_, constructor_args=None):
30-
if interface in self._registrations:
31-
raise ValueError(f"Dependency {interface} is already registered.")
32-
self._registrations[interface] = Registration(interface, class_, Scope.TRANSIENT, constructor_args)
33-
34-
def register_scoped(self, interface, class_, constructor_args=None):
35-
if interface in self._registrations:
36-
raise ValueError(f"Dependency {interface} is already registered.")
37-
self._registrations[interface] = Registration(interface, class_, Scope.SCOPED, constructor_args)
38-
39-
def register_singleton(self, interface, class_, constructor_args=None):
40-
if interface in self._registrations:
41-
raise ValueError(f"Dependency {interface} is already registered.")
42-
self._registrations[interface] = Registration(interface, class_, Scope.SINGLETON, constructor_args)
43-
44-
def resolve(self, interface, scope_name=DEFAULT_SCOPE_NAME):
32+
def register_transient(self, dependency: Type, implementation: Optional[Type] = None, tags: Optional[set] = None, constructor_args: Optional[Dict[str, Any]] = None) -> None:
33+
if implementation is None:
34+
implementation = dependency
35+
if dependency in self._registrations:
36+
raise ValueError(f"Dependency {dependency} is already registered.")
37+
self._registrations[dependency] = Registration(dependency, implementation, Scope.TRANSIENT, tags, constructor_args)
38+
39+
def register_scoped(self, dependency: Type, implementation: Optional[Type] = None, tags: Optional[set] = None, constructor_args: Optional[Dict[str, Any]] = None) -> None:
40+
if implementation is None:
41+
implementation = dependency
42+
if dependency in self._registrations:
43+
raise ValueError(f"Dependency {dependency} is already registered.")
44+
self._registrations[dependency] = Registration(dependency, implementation, Scope.SCOPED, tags, constructor_args)
45+
46+
def register_singleton(self, dependency: Type, implementation: Optional[Type] = None, tags: Optional[set] = None, constructor_args: Optional[Dict[str, Any]] = None) -> None:
47+
if implementation is None:
48+
implementation = dependency
49+
if dependency in self._registrations:
50+
raise ValueError(f"Dependency {dependency} is already registered.")
51+
self._registrations[dependency] = Registration(dependency, implementation, Scope.SINGLETON, tags, constructor_args)
52+
53+
def register_factory(self, dependency: Type, factory: Callable[[Any], Any], factory_args: Optional[Dict[str, Any]] = None, tags: Optional[set] = None) -> None:
54+
if dependency in self._registrations:
55+
raise ValueError(f"Dependency {dependency} is already registered.")
56+
self._registrations[dependency] = Registration(dependency, None, Scope.FACTORY, None, tags, factory, factory_args)
57+
58+
def register_instance(self, dependency: Type, instance: Any, tags: Optional[set] = None) -> None:
59+
if dependency in self._registrations:
60+
raise ValueError(f"Dependency {dependency} is already registered.")
61+
self._registrations[dependency] = Registration(dependency, type(instance), Scope.SINGLETON, constructor_args={}, tags=tags)
62+
self._singleton_instances[dependency] = instance
63+
64+
def resolve(self, dependency: Type, scope_name: str = DEFAULT_SCOPE_NAME) -> Type:
4565
if scope_name not in self._scoped_instances:
4666
self._scoped_instances[scope_name] = {}
4767

48-
if interface not in self._registrations:
49-
raise KeyError(f"Dependency {interface.__name__} is not registered.")
68+
if dependency not in self._registrations:
69+
raise KeyError(f"Dependency {dependency.__name__} is not registered.")
5070

51-
registration = self._registrations[interface]
52-
dependency_scope = registration.scope
53-
dependency_class = registration.class_
71+
registration = self._registrations[dependency]
72+
scope = registration.scope
73+
implementation = registration.implementation
5474
constructor_args = registration.constructor_args
5575

56-
self._validate_constructor_args(constructor_args=constructor_args, class_=dependency_class)
76+
self._validate_constructor_args(constructor_args=constructor_args, implementation=implementation)
5777

58-
if dependency_scope == Scope.TRANSIENT:
78+
if scope == Scope.TRANSIENT:
5979
return self._inject_dependencies(
60-
class_=dependency_class,
80+
implementation=implementation,
6181
constructor_args=constructor_args
6282
)
63-
elif dependency_scope == Scope.SCOPED:
64-
if interface not in self._scoped_instances[scope_name]:
65-
self._scoped_instances[scope_name][interface] = (
83+
elif scope == Scope.SCOPED:
84+
if dependency not in self._scoped_instances[scope_name]:
85+
self._scoped_instances[scope_name][dependency] = (
6686
self._inject_dependencies(
67-
class_=dependency_class,
87+
implementation=implementation,
6888
scope_name=scope_name,
6989
constructor_args=constructor_args,
7090
))
71-
return self._scoped_instances[scope_name][interface]
72-
elif dependency_scope == Scope.SINGLETON:
73-
if interface not in self._singleton_instances:
74-
self._singleton_instances[interface] = (
91+
return self._scoped_instances[scope_name][dependency]
92+
elif scope == Scope.SINGLETON:
93+
if dependency not in self._singleton_instances:
94+
self._singleton_instances[dependency] = (
7595
self._inject_dependencies(
76-
class_=dependency_class,
96+
implementation=implementation,
7797
constructor_args=constructor_args
7898
)
7999
)
80-
return self._singleton_instances[interface]
81-
82-
raise ValueError(f"Invalid dependency scope: {dependency_scope}")
83-
84-
def _validate_constructor_args(self, constructor_args: Dict[str, Any], class_: Type) -> None:
85-
class_constructor = inspect.signature(class_.__init__).parameters
100+
return self._singleton_instances[dependency]
101+
elif scope == Scope.FACTORY:
102+
factory = registration.factory
103+
factory_args = registration.factory_args or {}
104+
return factory(**factory_args)
105+
106+
raise ValueError(f"Invalid dependency scope: {scope}")
107+
108+
def resolve_all(self, tags: Optional[set] = None) -> List[Any]:
109+
tags = tags or []
110+
resolved_dependencies = []
111+
for registration in self._registrations.values():
112+
if not len(tags) or tags.intersection(registration.tags):
113+
resolved_dependencies.append(
114+
self.resolve(registration.dependency))
115+
return resolved_dependencies
116+
117+
def _validate_constructor_args(self, constructor_args: Dict[str, Any], implementation: Type) -> None:
118+
constructor = inspect.signature(implementation.__init__).parameters
86119

87120
# Check if any required parameter is missing
88-
missing_params = [param for param in class_constructor.keys() if
121+
missing_params = [param for param in constructor.keys() if
89122
param not in ["self", "cls", "args", "kwargs"] and
90123
param not in constructor_args]
91124
if missing_params:
92125
raise ValueError(
93126
f"Missing required constructor arguments: "
94-
f"{', '.join(missing_params)} for class '{class_.__name__}'.")
127+
f"{', '.join(missing_params)} for class '{implementation.__name__}'.")
95128

96129
for arg_name, arg_value in constructor_args.items():
97-
if arg_name not in class_constructor:
130+
if arg_name not in constructor:
98131
raise ValueError(
99-
f"Invalid constructor argument '{arg_name}' for class '{class_.__name__}'. "
132+
f"Invalid constructor argument '{arg_name}' for class '{implementation.__name__}'. "
100133
f"The class does not have a constructor parameter with this name.")
101134

102-
expected_type = class_constructor[arg_name].annotation
135+
expected_type = constructor[arg_name].annotation
103136
if expected_type != inspect.Parameter.empty:
104137
if not isinstance(arg_value, expected_type):
105138
raise TypeError(
106139
f"Constructor argument '{arg_name}' has an incompatible type. "
107140
f"Expected type: {expected_type}, provided type: {type(arg_value)}.")
108141

109-
def _inject_dependencies(self, class_, scope_name=None, constructor_args=None):
110-
constructor = inspect.signature(class_.__init__)
142+
def _inject_dependencies(self, implementation: Type, scope_name: str = None, constructor_args: Optional[Dict[str, Any]] = None) -> Type:
143+
constructor = inspect.signature(implementation.__init__)
111144
params = constructor.parameters
112145

113146
dependencies = {}
@@ -127,4 +160,4 @@ def _inject_dependencies(self, class_, scope_name=None, constructor_args=None):
127160
else:
128161
dependencies[param_name] = self.resolve(param_info.annotation, scope_name=scope_name)
129162

130-
return class_(**dependencies)
163+
return implementation(**dependencies)

src/dependency_injection/decorator.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,25 @@
11
import functools
22
import inspect
3+
from typing import Any, Callable, TypeVar
34

45
from dependency_injection.container import DEFAULT_CONTAINER_NAME, \
56
DependencyContainer
67
from dependency_injection.scope import DEFAULT_SCOPE_NAME
78

9+
F = TypeVar('F', bound=Callable[..., Any])
810

9-
def inject(container_name=DEFAULT_CONTAINER_NAME, scope_name=DEFAULT_SCOPE_NAME):
1011

11-
def is_instance_method(func):
12+
def inject(container_name=DEFAULT_CONTAINER_NAME, scope_name=DEFAULT_SCOPE_NAME) -> Callable[[F], F]:
13+
14+
def is_instance_method(func: Callable[..., Any]) -> bool:
1215
parameters = inspect.signature(func).parameters
1316
is_instance_method = len(parameters) > 0 and list(parameters.values())[0].name == 'self'
1417
return is_instance_method
1518

16-
def decorator_inject(func):
19+
def decorator_inject(func: F) -> F:
1720

1821
@functools.wraps(func)
19-
def wrapper_inject(*args, **kwargs):
22+
def wrapper_inject(*args: Any, **kwargs: Any) -> Any:
2023

2124
# Get the parameter names from the function signature
2225
sig = inspect.signature(func)
Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,18 @@
1-
from typing import Any, Dict
1+
from typing import Any, Callable, Dict, Optional, Type
22

33
from dependency_injection.scope import Scope
44

55

66
class Registration():
77

8-
def __init__(self, interface, class_, scope: Scope, constructor_args: Dict[str, Any] = None):
9-
self.interface = interface
10-
self.class_ = class_
8+
def __init__(self, dependency: Type, implementation: Optional[Type], scope: Scope, tags: Optional[set] = None, constructor_args: Optional[Dict[str, Any]] = None, factory: Optional[Callable[[Any], Any]] = None, factory_args: Optional[Dict[str, Any]] = None):
9+
self.dependency = dependency
10+
self.implementation = implementation
1111
self.scope = scope
12+
self.tags = tags or set()
1213
self.constructor_args = constructor_args or {}
14+
self.factory = factory
15+
self.factory_args = factory_args or {}
16+
17+
if not any([self.implementation, self.factory]):
18+
raise Exception("There must be either an implementation or a factory.")

src/dependency_injection/scope.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,4 @@ class Scope(Enum):
77
TRANSIENT = "transient"
88
SCOPED = "scoped"
99
SINGLETON = "singleton"
10+
FACTORY = "factory"

0 commit comments

Comments
 (0)