Skip to content

Commit ca58b47

Browse files
authored
Merge pull request #8 from runemalm/feature/next-version
Release v1.0.0-alpha.10
2 parents 38801bf + 33dcc25 commit ca58b47

File tree

12 files changed

+431
-62
lines changed

12 files changed

+431
-62
lines changed

README.md

+4
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,10 @@ You can find the source code for `py-dependency-injection` on [GitHub](https://g
8383

8484
## Release Notes
8585

86+
### [1.0.0-alpha.10](https://github.com/runemalm/py-dependency-injection/releases/tag/v1.0.0-alpha.10) (2024-08-11)
87+
88+
- **Tagged Constructor Injection**: Introduced support for constructor injection using the `Tagged`, `AnyTagged`, and `AllTagged` classes. This allows for seamless injection of dependencies that have been registered with specific tags, enhancing flexibility and control in managing your application's dependencies.
89+
8690
### [1.0.0-alpha.9](https://github.com/runemalm/py-dependency-injection/releases/tag/v1.0.0-alpha.9) (2024-08-08)
8791

8892
- **Breaking Change**: Removed constructor injection when resolving dataclasses.

docs/conf.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@
3535
version = "1.0"
3636

3737
# The full version, including alpha/beta/rc tags
38-
release = "1.0.0-alpha.9"
38+
release = "1.0.0-alpha.10"
3939

4040

4141
# -- General configuration ---------------------------------------------------

docs/examples.rst

+50
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,56 @@ This example illustrates how to use constructor injection to automatically injec
225225
print(repository.connection.__class__.__name__) # Output: PostgresConnection
226226
227227
228+
######################################################
229+
Using constructor injection with tagged dependencies
230+
######################################################
231+
232+
This example demonstrates how to use constructor injection to automatically inject tagged dependencies into your classes. By leveraging tags, you can group and categorize dependencies, enabling automatic injection based on specific criteria.
233+
234+
.. code-block:: python
235+
236+
class PrimaryPort:
237+
pass
238+
239+
class SecondaryPort:
240+
pass
241+
242+
class HttpAdapter(PrimaryPort):
243+
pass
244+
245+
class PostgresCarRepository(SecondaryPort):
246+
pass
247+
248+
class Application:
249+
def __init__(self, primary_ports: List[Tagged[PrimaryPort]], secondary_ports: List[Tagged[SecondaryPort]]):
250+
self.primary_ports = primary_ports
251+
self.secondary_ports = secondary_ports
252+
253+
# Register dependencies with tags
254+
dependency_container.register_transient(HttpAdapter, tags={PrimaryPort})
255+
dependency_container.register_transient(PostgresCarRepository, tags={SecondaryPort})
256+
257+
# Register the Application class to have its dependencies injected
258+
dependency_container.register_transient(Application)
259+
260+
# Resolve the Application class, with tagged dependencies automatically injected
261+
application = dependency_container.resolve(Application)
262+
263+
# Use the injected dependencies
264+
print(f"Primary ports: {len(application.primary_ports)}") # Output: Primary ports: 1
265+
print(f"Secondary ports: {len(application.secondary_ports)}") # Output: Secondary ports: 1
266+
print(f"Primary port instance: {type(application.primary_ports[0]).__name__}") # Output: HttpAdapter
267+
print(f"Secondary port instance: {type(application.secondary_ports[0]).__name__}") # Output: PostgresCarRepository
268+
269+
270+
In this example, the `Application` class expects lists of instances tagged with `PrimaryPort` and `SecondaryPort`. By tagging and registering these dependencies, the container automatically injects the correct instances into the `Application` class when it is resolved.
271+
272+
Tags offer a powerful way to manage dependencies, ensuring that the right instances are injected based on your application's needs.
273+
274+
.. note::
275+
You can also use the ``AnyTagged`` and ``AllTagged`` classes to inject dependencies based on more complex tagging logic. ``AnyTagged`` allows injection of any dependency matching one or more specified tags, while ``AllTagged`` requires the dependency to match all specified tags before injection. This provides additional flexibility in managing and resolving dependencies in your application.
276+
277+
228278
######################
229279
Using method injection
230280
######################

docs/releases.rst

+6
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,12 @@
66
Version History
77
###############
88

9+
**1.0.0-alpha.10 (2024-08-11)**
10+
11+
- **Tagged Constructor Injection**: Introduced support for constructor injection using the `Tagged`, `AnyTagged`, and `AllTagged` classes. This allows for seamless injection of dependencies that have been registered with specific tags, enhancing flexibility and control in managing your application's dependencies.
12+
13+
`View release on GitHub <https://github.com/runemalm/py-dependency-injection/releases/tag/v1.0.0-alpha.10>`_
14+
915
**1.0.0-alpha.9 (2024-08-08)**
1016

1117
- **Breaking Change**: Removed constructor injection when resolving dataclasses.

setup.py

+1-1
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.9",
9+
version="1.0.0-alpha.10",
1010
author="David Runemalm, 2024",
1111
author_email="[email protected]",
1212
description="A dependency injection library for Python.",

src/dependency_injection/container.py

+119-60
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@
33

44
from typing import Any, Callable, Dict, List, Optional, TypeVar, Type
55

6+
from dependency_injection.tags.all_tagged import AllTagged
7+
from dependency_injection.tags.any_tagged import AnyTagged
8+
from dependency_injection.tags.tagged import Tagged
69
from dependency_injection.registration import Registration
710
from dependency_injection.scope import DEFAULT_SCOPE_NAME, Scope
811
from dependency_injection.utils.singleton_meta import SingletonMeta
@@ -23,8 +26,7 @@ def __init__(self, name: str = None):
2326

2427
@classmethod
2528
def get_instance(cls, name: str = None) -> Self:
26-
if name is None:
27-
name = DEFAULT_CONTAINER_NAME
29+
name = name or DEFAULT_CONTAINER_NAME
2830

2931
if (cls, name) not in cls._instances:
3032
cls._instances[(cls, name)] = cls(name)
@@ -48,11 +50,7 @@ def register_transient(
4850
tags: Optional[set] = None,
4951
constructor_args: Optional[Dict[str, Any]] = None,
5052
) -> None:
51-
if implementation is None:
52-
implementation = dependency
53-
if dependency in self._registrations:
54-
raise ValueError(f"Dependency {dependency} is already registered.")
55-
self._registrations[dependency] = Registration(
53+
self._register(
5654
dependency, implementation, Scope.TRANSIENT, tags, constructor_args
5755
)
5856

@@ -63,13 +61,7 @@ def register_scoped(
6361
tags: Optional[set] = None,
6462
constructor_args: Optional[Dict[str, Any]] = None,
6563
) -> None:
66-
if implementation is None:
67-
implementation = dependency
68-
if dependency in self._registrations:
69-
raise ValueError(f"Dependency {dependency} is already registered.")
70-
self._registrations[dependency] = Registration(
71-
dependency, implementation, Scope.SCOPED, tags, constructor_args
72-
)
64+
self._register(dependency, implementation, Scope.SCOPED, tags, constructor_args)
7365

7466
def register_singleton(
7567
self,
@@ -78,11 +70,7 @@ def register_singleton(
7870
tags: Optional[set] = None,
7971
constructor_args: Optional[Dict[str, Any]] = None,
8072
) -> None:
81-
if implementation is None:
82-
implementation = dependency
83-
if dependency in self._registrations:
84-
raise ValueError(f"Dependency {dependency} is already registered.")
85-
self._registrations[dependency] = Registration(
73+
self._register(
8674
dependency, implementation, Scope.SINGLETON, tags, constructor_args
8775
)
8876

@@ -93,73 +81,104 @@ def register_factory(
9381
factory_args: Optional[Dict[str, Any]] = None,
9482
tags: Optional[set] = None,
9583
) -> None:
96-
if dependency in self._registrations:
97-
raise ValueError(f"Dependency {dependency} is already registered.")
84+
self._validate_registration(dependency)
9885
self._registrations[dependency] = Registration(
99-
dependency, None, Scope.FACTORY, None, tags, factory, factory_args
86+
dependency, None, Scope.FACTORY, tags, None, factory, factory_args
10087
)
10188

10289
def register_instance(
10390
self, dependency: Type, instance: Any, tags: Optional[set] = None
10491
) -> None:
105-
if dependency in self._registrations:
106-
raise ValueError(f"Dependency {dependency} is already registered.")
92+
self._validate_registration(dependency)
10793
self._registrations[dependency] = Registration(
108-
dependency, type(instance), Scope.SINGLETON, constructor_args={}, tags=tags
94+
dependency, type(instance), Scope.SINGLETON, tags=tags
10995
)
11096
self._singleton_instances[dependency] = instance
11197

112-
def resolve(self, dependency: Type, scope_name: str = DEFAULT_SCOPE_NAME) -> Type:
98+
def _register(
99+
self,
100+
dependency: Type,
101+
implementation: Optional[Type],
102+
scope: Scope,
103+
tags: Optional[set],
104+
constructor_args: Optional[Dict[str, Any]],
105+
) -> None:
106+
implementation = implementation or dependency
107+
self._validate_registration(dependency)
108+
self._registrations[dependency] = Registration(
109+
dependency, implementation, scope, tags, constructor_args
110+
)
111+
112+
def resolve(self, dependency: Type, scope_name: str = DEFAULT_SCOPE_NAME) -> Any:
113113
self._has_resolved = True
114114

115115
if scope_name not in self._scoped_instances:
116116
self._scoped_instances[scope_name] = {}
117117

118-
if dependency not in self._registrations:
118+
registration = self._registrations.get(dependency)
119+
if not registration:
119120
raise KeyError(f"Dependency {dependency.__name__} is not registered.")
120121

121-
registration = self._registrations[dependency]
122-
scope = registration.scope
123-
implementation = registration.implementation
124-
constructor_args = registration.constructor_args
122+
constructor_args = registration.constructor_args or {}
123+
self._validate_constructor_args(constructor_args, registration.implementation)
125124

126-
self._validate_constructor_args(
127-
constructor_args=constructor_args, implementation=implementation
128-
)
125+
return self._resolve_by_scope(registration, scope_name)
126+
127+
def _resolve_by_scope(self, registration: Registration, scope_name: str) -> Any:
128+
scope = registration.scope
129129

130130
if scope == Scope.TRANSIENT:
131131
return self._inject_dependencies(
132-
implementation=implementation, constructor_args=constructor_args
132+
registration.implementation,
133+
constructor_args=registration.constructor_args,
133134
)
134135
elif scope == Scope.SCOPED:
135-
if dependency not in self._scoped_instances[scope_name]:
136-
self._scoped_instances[scope_name][
137-
dependency
138-
] = self._inject_dependencies(
139-
implementation=implementation,
140-
scope_name=scope_name,
141-
constructor_args=constructor_args,
136+
instances = self._scoped_instances[scope_name]
137+
if registration.dependency not in instances:
138+
instances[registration.dependency] = self._inject_dependencies(
139+
registration.implementation,
140+
scope_name,
141+
registration.constructor_args,
142142
)
143-
return self._scoped_instances[scope_name][dependency]
143+
return instances[registration.dependency]
144144
elif scope == Scope.SINGLETON:
145-
if dependency not in self._singleton_instances:
146-
self._singleton_instances[dependency] = self._inject_dependencies(
147-
implementation=implementation, constructor_args=constructor_args
145+
if registration.dependency not in self._singleton_instances:
146+
self._singleton_instances[
147+
registration.dependency
148+
] = self._inject_dependencies(
149+
registration.implementation,
150+
constructor_args=registration.constructor_args,
148151
)
149-
return self._singleton_instances[dependency]
152+
return self._singleton_instances[registration.dependency]
150153
elif scope == Scope.FACTORY:
151-
factory = registration.factory
152-
factory_args = registration.factory_args or {}
153-
return factory(**factory_args)
154+
return registration.factory(**(registration.factory_args or {}))
154155

155156
raise ValueError(f"Invalid dependency scope: {scope}")
156157

157-
def resolve_all(self, tags: Optional[set] = None) -> List[Any]:
158-
tags = tags or []
158+
def resolve_all(
159+
self, tags: Optional[set] = None, match_all_tags: bool = False
160+
) -> List[Any]:
161+
tags = tags or set()
159162
resolved_dependencies = []
163+
160164
for registration in self._registrations.values():
161-
if not len(tags) or tags.intersection(registration.tags):
165+
if not tags:
166+
# If no tags are provided, resolve all dependencies
162167
resolved_dependencies.append(self.resolve(registration.dependency))
168+
else:
169+
if match_all_tags:
170+
# Match dependencies that have all the specified tags
171+
if registration.tags and tags.issubset(registration.tags):
172+
resolved_dependencies.append(
173+
self.resolve(registration.dependency)
174+
)
175+
else:
176+
# Match dependencies that have any of the specified tags
177+
if registration.tags and tags.intersection(registration.tags):
178+
resolved_dependencies.append(
179+
self.resolve(registration.dependency)
180+
)
181+
163182
return resolved_dependencies
164183

165184
def _validate_constructor_args(
@@ -184,6 +203,10 @@ def _validate_constructor_args(
184203
f"provided type: {type(arg_value)}."
185204
)
186205

206+
def _validate_registration(self, dependency: Type) -> None:
207+
if dependency in self._registrations:
208+
raise ValueError(f"Dependency {dependency} is already registered.")
209+
187210
def _inject_dependencies(
188211
self,
189212
implementation: Type,
@@ -199,20 +222,56 @@ def _inject_dependencies(
199222
dependencies = {}
200223
for param_name, param_info in params.items():
201224
if param_name != "self":
202-
# Check for *args and **kwargs
203225
if param_info.kind == inspect.Parameter.VAR_POSITIONAL:
204-
# *args parameter
205226
pass
206227
elif param_info.kind == inspect.Parameter.VAR_KEYWORD:
207-
# **kwargs parameter
208228
pass
209229
else:
210-
# Check if constructor_args has an argument with the same name
211230
if constructor_args and param_name in constructor_args:
212231
dependencies[param_name] = constructor_args[param_name]
213232
else:
214-
dependencies[param_name] = self.resolve(
215-
param_info.annotation, scope_name=scope_name
216-
)
233+
if (
234+
hasattr(param_info.annotation, "__origin__")
235+
and param_info.annotation.__origin__ is list
236+
):
237+
inner_type = param_info.annotation.__args__[0]
238+
239+
tagged_dependencies = []
240+
if isinstance(inner_type, type) and issubclass(
241+
inner_type, Tagged
242+
):
243+
tagged_type = inner_type.tag
244+
tagged_dependencies = self.resolve_all(
245+
tags={tagged_type}
246+
)
247+
248+
elif isinstance(inner_type, type) and issubclass(
249+
inner_type, AnyTagged
250+
):
251+
tagged_dependencies = self.resolve_all(
252+
tags=inner_type.tags, match_all_tags=False
253+
)
254+
255+
elif isinstance(inner_type, type) and issubclass(
256+
inner_type, AllTagged
257+
):
258+
tagged_dependencies = self.resolve_all(
259+
tags=inner_type.tags, match_all_tags=True
260+
)
261+
262+
dependencies[param_name] = tagged_dependencies
263+
264+
else:
265+
try:
266+
dependencies[param_name] = self.resolve(
267+
param_info.annotation, scope_name=scope_name
268+
)
269+
except KeyError:
270+
raise ValueError(
271+
f"Cannot resolve dependency for parameter "
272+
f"'{param_name}' of type "
273+
f"'{param_info.annotation}' in class "
274+
f"'{implementation.__name__}'."
275+
)
217276

218277
return implementation(**dependencies)

src/dependency_injection/tags/__init__.py

Whitespace-only changes.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
from typing import Generic, Set, Tuple, Type, TypeVar, Union
2+
3+
T = TypeVar("T")
4+
5+
6+
class AllTagged(Generic[T]):
7+
def __init__(self, tags: Tuple[Type[T], ...]):
8+
self.tags: Set[Type[T]] = set(tags)
9+
10+
@classmethod
11+
def __class_getitem__(
12+
cls, item: Union[Type[T], Tuple[Type[T], ...]]
13+
) -> Type["AllTagged"]:
14+
if not isinstance(item, tuple):
15+
item = (item,)
16+
return type(
17+
f'AllTagged_{"_".join([t.__name__ for t in item])}',
18+
(cls,),
19+
{"tags": set(item)},
20+
)

0 commit comments

Comments
 (0)