Skip to content

Commit a4a84be

Browse files
authored
Wiring by string id (#403)
* Add prototype implementation * Implement wiring by string id * Fix pydocstyle errors * Refactor wiring module * Fix flake8 errors * Update changelog * Fix flake8 errors * Add example and docs
1 parent d9d811a commit a4a84be

File tree

15 files changed

+934
-9
lines changed

15 files changed

+934
-9
lines changed

docs/main/changelog.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ follows `Semantic versioning`_
99

1010
Development version
1111
-------------------
12+
- Add wiring by string id.
1213
- Improve error message for ``Dependency`` provider missing attribute.
1314

1415
4.25.1

docs/wiring.rst

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,82 @@ Also you can use ``Provide`` marker to inject a container.
8888
:emphasize-lines: 16-19
8989
:lines: 3-
9090

91+
Strings identifiers
92+
-------------------
93+
94+
You can use wiring with string identifiers. String identifier should match provider name in the container:
95+
96+
.. literalinclude:: ../examples/wiring/example_string_id.py
97+
:language: python
98+
:emphasize-lines: 17
99+
:lines: 3-
100+
101+
With string identifiers you don't need to use a container to specify an injection.
102+
103+
To specify an injection from a nested container use point ``.`` as a separator:
104+
105+
.. code-block:: python
106+
107+
@inject
108+
def foo(service: UserService = Provide['services.user']) -> None:
109+
...
110+
111+
You can also use injection modifiers:
112+
113+
.. code-block:: python
114+
115+
from dependency_injector.wiring import (
116+
inject,
117+
Provide,
118+
as_int,
119+
as_float,
120+
as_,
121+
required,
122+
invariant,
123+
provided,
124+
)
125+
126+
127+
@inject
128+
def foo(value: int = Provide['config.option', as_int()]) -> None:
129+
...
130+
131+
132+
@inject
133+
def foo(value: float = Provide['config.option', as_float()]) -> None:
134+
...
135+
136+
137+
@inject
138+
def foo(value: Decimal = Provide['config.option', as_(Decimal)]) -> None:
139+
...
140+
141+
@inject
142+
def foo(value: str = Provide['config.option', required()]) -> None:
143+
...
144+
145+
@inject
146+
def foo(value: int = Provide['config.option', required().as_int()]) -> None:
147+
...
148+
149+
150+
@inject
151+
def foo(value: int = Provide['config.option', invariant('config.switch')]) -> None:
152+
...
153+
154+
@inject
155+
def foo(value: int = Provide['service', provided().foo['bar'].call()]) -> None:
156+
...
157+
158+
159+
To inject a container use special identifier ``<container>``:
160+
161+
.. code-block:: python
162+
163+
@inject
164+
def foo(container: Container = Provide['<container>']) -> None:
165+
...
166+
91167
Wiring with modules and packages
92168
--------------------------------
93169

examples/wiring/example_string_id.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
"""Wiring string id example."""
2+
3+
import sys
4+
5+
from dependency_injector import containers, providers
6+
from dependency_injector.wiring import inject, Provide
7+
8+
9+
class Service:
10+
...
11+
12+
13+
class Container(containers.DeclarativeContainer):
14+
15+
service = providers.Factory(Service)
16+
17+
18+
@inject
19+
def main(service: Service = Provide['service']) -> None:
20+
...
21+
22+
23+
if __name__ == '__main__':
24+
container = Container()
25+
container.wire(modules=[sys.modules[__name__]])
26+
27+
main()

src/dependency_injector/wiring.py

Lines changed: 184 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,12 @@ class GenericMeta(type):
5050
'wire',
5151
'unwire',
5252
'inject',
53+
'as_int',
54+
'as_float',
55+
'as_',
56+
'required',
57+
'invariant',
58+
'provided',
5359
'Provide',
5460
'Provider',
5561
'Closing',
@@ -85,16 +91,23 @@ def get_from_module(self, module: ModuleType) -> Iterator[Callable[..., Any]]:
8591

8692
class ProvidersMap:
8793

94+
CONTAINER_STRING_ID = '<container>'
95+
8896
def __init__(self, container):
8997
self._container = container
9098
self._map = self._create_providers_map(
9199
current_container=container,
92-
original_container=container.declarative_parent,
100+
original_container=(
101+
container.declarative_parent
102+
if container.declarative_parent
103+
else container
104+
),
93105
)
94106

95107
def resolve_provider(
96108
self,
97-
provider: providers.Provider,
109+
provider: Union[providers.Provider, str],
110+
modifier: Optional['Modifier'] = None,
98111
) -> Optional[providers.Provider]:
99112
if isinstance(provider, providers.Delegate):
100113
return self._resolve_delegate(provider)
@@ -109,14 +122,29 @@ def resolve_provider(
109122
return self._resolve_config_option(provider)
110123
elif isinstance(provider, providers.TypedConfigurationOption):
111124
return self._resolve_config_option(provider.option, as_=provider.provides)
125+
elif isinstance(provider, str):
126+
return self._resolve_string_id(provider, modifier)
112127
else:
113128
return self._resolve_provider(provider)
114129

115-
def _resolve_delegate(
130+
def _resolve_string_id(
116131
self,
117-
original: providers.Delegate,
132+
id: str,
133+
modifier: Optional['Modifier'] = None,
118134
) -> Optional[providers.Provider]:
119-
return self._resolve_provider(original.provides)
135+
if id == self.CONTAINER_STRING_ID:
136+
return self._container.__self__
137+
138+
provider = self._container
139+
for segment in id.split('.'):
140+
try:
141+
provider = getattr(provider, segment)
142+
except AttributeError:
143+
return None
144+
145+
if modifier:
146+
provider = modifier.modify(provider, providers_map=self)
147+
return provider
120148

121149
def _resolve_provided_instance(
122150
self,
@@ -151,6 +179,12 @@ def _resolve_provided_instance(
151179

152180
return new
153181

182+
def _resolve_delegate(
183+
self,
184+
original: providers.Delegate,
185+
) -> Optional[providers.Provider]:
186+
return self._resolve_provider(original.provides)
187+
154188
def _resolve_config_option(
155189
self,
156190
original: providers.ConfigurationOption,
@@ -184,7 +218,7 @@ def _resolve_provider(
184218
try:
185219
return self._map[original]
186220
except KeyError:
187-
pass
221+
return None
188222

189223
@classmethod
190224
def _create_providers_map(
@@ -381,7 +415,7 @@ def _fetch_reference_injections(
381415

382416
def _bind_injections(fn: Callable[..., Any], providers_map: ProvidersMap) -> None:
383417
for injection, marker in fn.__reference_injections__.items():
384-
provider = providers_map.resolve_provider(marker.provider)
418+
provider = providers_map.resolve_provider(marker.provider, marker.modifier)
385419

386420
if provider is None:
387421
continue
@@ -516,20 +550,161 @@ def _is_declarative_container(instance: Any) -> bool:
516550
and getattr(instance, 'declarative_parent', None) is None)
517551

518552

553+
class Modifier:
554+
555+
def modify(
556+
self,
557+
provider: providers.ConfigurationOption,
558+
providers_map: ProvidersMap,
559+
) -> providers.Provider:
560+
...
561+
562+
563+
class TypeModifier(Modifier):
564+
565+
def __init__(self, type_: Type):
566+
self.type_ = type_
567+
568+
def modify(
569+
self,
570+
provider: providers.ConfigurationOption,
571+
providers_map: ProvidersMap,
572+
) -> providers.Provider:
573+
return provider.as_(self.type_)
574+
575+
576+
def as_int() -> TypeModifier:
577+
"""Return int type modifier."""
578+
return TypeModifier(int)
579+
580+
581+
def as_float() -> TypeModifier:
582+
"""Return float type modifier."""
583+
return TypeModifier(float)
584+
585+
586+
def as_(type_: Type) -> TypeModifier:
587+
"""Return custom type modifier."""
588+
return TypeModifier(type_)
589+
590+
591+
class RequiredModifier(Modifier):
592+
593+
def __init__(self):
594+
self.type_modifier = None
595+
596+
def as_int(self) -> 'RequiredModifier':
597+
self.type_modifier = TypeModifier(int)
598+
return self
599+
600+
def as_float(self) -> 'RequiredModifier':
601+
self.type_modifier = TypeModifier(float)
602+
return self
603+
604+
def as_(self, type_: Type) -> 'RequiredModifier':
605+
self.type_modifier = TypeModifier(type_)
606+
return self
607+
608+
def modify(
609+
self,
610+
provider: providers.ConfigurationOption,
611+
providers_map: ProvidersMap,
612+
) -> providers.Provider:
613+
provider = provider.required()
614+
if self.type_modifier:
615+
provider = provider.as_(self.type_modifier.type_)
616+
return provider
617+
618+
619+
def required() -> RequiredModifier:
620+
"""Return required modifier."""
621+
return RequiredModifier()
622+
623+
624+
class InvariantModifier(Modifier):
625+
626+
def __init__(self, id: str) -> None:
627+
self.id = id
628+
629+
def modify(
630+
self,
631+
provider: providers.ConfigurationOption,
632+
providers_map: ProvidersMap,
633+
) -> providers.Provider:
634+
invariant_segment = providers_map.resolve_provider(self.id)
635+
return provider[invariant_segment]
636+
637+
638+
def invariant(id: str) -> InvariantModifier:
639+
"""Return invariant modifier."""
640+
return InvariantModifier(id)
641+
642+
643+
class ProvidedInstance(Modifier):
644+
645+
TYPE_ATTRIBUTE = 'attr'
646+
TYPE_ITEM = 'item'
647+
TYPE_CALL = 'call'
648+
649+
def __init__(self):
650+
self.segments = []
651+
652+
def __getattr__(self, item):
653+
self.segments.append((self.TYPE_ATTRIBUTE, item))
654+
return self
655+
656+
def __getitem__(self, item):
657+
self.segments.append((self.TYPE_ITEM, item))
658+
return self
659+
660+
def call(self):
661+
self.segments.append((self.TYPE_CALL, None))
662+
return self
663+
664+
def modify(
665+
self,
666+
provider: providers.ConfigurationOption,
667+
providers_map: ProvidersMap,
668+
) -> providers.Provider:
669+
provider = provider.provided
670+
for type_, value in self.segments:
671+
if type_ == ProvidedInstance.TYPE_ATTRIBUTE:
672+
provider = getattr(provider, value)
673+
elif type_ == ProvidedInstance.TYPE_ITEM:
674+
provider = provider[value]
675+
elif type_ == ProvidedInstance.TYPE_CALL:
676+
provider = provider.call()
677+
return provider
678+
679+
680+
def provided() -> ProvidedInstance:
681+
"""Return provided instance modifier."""
682+
return ProvidedInstance()
683+
684+
519685
class ClassGetItemMeta(GenericMeta):
520686
def __getitem__(cls, item):
521687
# Spike for Python 3.6
688+
if isinstance(item, tuple):
689+
return cls(*item)
522690
return cls(item)
523691

524692

525693
class _Marker(Generic[T], metaclass=ClassGetItemMeta):
526694

527-
def __init__(self, provider: Union[providers.Provider, Container]) -> None:
695+
def __init__(
696+
self,
697+
provider: Union[providers.Provider, Container, str],
698+
modifier: Optional[Modifier] = None,
699+
) -> None:
528700
if _is_declarative_container(provider):
529701
provider = provider.__self__
530-
self.provider: providers.Provider = provider
702+
self.provider = provider
703+
self.modifier = modifier
531704

532705
def __class_getitem__(cls, item) -> T:
706+
if isinstance(item, tuple):
707+
return cls(*item)
533708
return cls(item)
534709

535710
def __call__(self) -> T:

tests/unit/samples/wiringstringidssamples/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)