23
23
import types
24
24
from abc import ABCMeta , abstractmethod
25
25
from collections import namedtuple
26
- from typing import (
27
- Any ,
28
- Callable ,
29
- cast ,
30
- Dict ,
31
- Generic ,
32
- get_type_hints ,
33
- List ,
34
- overload ,
35
- Tuple ,
36
- Type ,
37
- TypeVar ,
38
- Union ,
39
- )
26
+ from typing import Any , Callable , cast , Dict , Generic , List , overload , Tuple , Type , TypeVar , Union
27
+
28
+ # Ignoring errors here as typing_extensions stub doesn't know about those things yet
29
+ from typing_extensions import _AnnotatedAlias , Annotated , get_type_hints # type: ignore
40
30
41
31
TYPING353 = hasattr (Union [str , int ], '__origin__' )
42
32
@@ -79,6 +69,80 @@ def wrapper(*args: Any, **kwargs: Any) -> Any:
79
69
lock = threading .RLock ()
80
70
81
71
72
+ _inject_marker = object ()
73
+ _noinject_marker = object ()
74
+
75
+ InjectT = TypeVar ('InjectT' )
76
+ Inject = Annotated [InjectT , _inject_marker ]
77
+ """An experimental way to declare injectable dependencies utilizing a `PEP 593`_ implementation
78
+ in `typing_extensions`. This API is likely to change when `PEP 593`_ stabilizes.
79
+
80
+ Those two declarations are equivalent::
81
+
82
+ @inject
83
+ def fun(t: SomeType) -> None:
84
+ pass
85
+
86
+ def fun(t: Inject[SomeType]) -> None:
87
+ pass
88
+
89
+ The advantage over using :func:`inject` is that if you have some noninjectable parameters
90
+ it may be easier to spot what are they. Those two are equivalent::
91
+
92
+ @inject
93
+ @noninjectable('s')
94
+ def fun(t: SomeType, s: SomeOtherType) -> None:
95
+ pass
96
+
97
+ def fun(t: Inject[SomeType], s: SomeOtherType) -> None:
98
+ pass
99
+
100
+ .. seealso::
101
+
102
+ Function :func:`get_bindings`
103
+ A way to inspect how various injection declarations interact with each other.
104
+
105
+ .. versionadded:: 0.18.0
106
+
107
+ .. _PEP 593: https://www.python.org/dev/peps/pep-0593/
108
+ .. _typing_extensions: https://pypi.org/project/typing-extensions/
109
+ """
110
+
111
+ NoInject = Annotated [InjectT , _noinject_marker ]
112
+ """An experimental way to declare noninjectable dependencies utilizing a `PEP 593`_ implementation
113
+ in `typing_extensions`. This API is likely to change when `PEP 593`_ stabilizes.
114
+
115
+ Since :func:`inject` declares all function's parameters to be injectable there needs to be a way
116
+ to opt out of it. This has been provided by :func:`noninjectable` but `noninjectable` suffers from
117
+ two issues:
118
+
119
+ * You need to repeat the parameter name
120
+ * The declaration may be relatively distance in space from the actual parameter declaration, thus
121
+ hindering readability
122
+
123
+ `NoInject` solves both of those concerns, for example (those two declarations are equivalent)::
124
+
125
+ @inject
126
+ @noninjectable('b')
127
+ def fun(a: TypeA, b: TypeB) -> None:
128
+ pass
129
+
130
+ @inject
131
+ def fun(a: TypeA, b: NoInject[TypeB]) -> None:
132
+ pass
133
+
134
+ .. seealso::
135
+
136
+ Function :func:`get_bindings`
137
+ A way to inspect how various injection declarations interact with each other.
138
+
139
+ .. versionadded:: 0.18.0
140
+
141
+ .. _PEP 593: https://www.python.org/dev/peps/pep-0593/
142
+ .. _typing_extensions: https://pypi.org/project/typing-extensions/
143
+ """
144
+
145
+
82
146
def reraise (original : Exception , exception : Exception , maximum_frames : int = 1 ) -> None :
83
147
prev_cls , prev , tb = sys .exc_info ()
84
148
frames = inspect .getinnerframes (cast (types .TracebackType , tb ))
@@ -511,6 +575,13 @@ def _is_specialization(cls, generic_class):
511
575
# issubclass(SomeGeneric[X], SomeGeneric) so we need some other way to
512
576
# determine whether a particular object is a generic class with type parameters
513
577
# provided. Fortunately there seems to be __origin__ attribute that's useful here.
578
+
579
+ # We need to special-case Annotated as its __origin__ behaves differently than
580
+ # other typing generic classes. See https://github.com/python/typing/pull/635
581
+ # for some details.
582
+ if generic_class is Annotated and isinstance (cls , _AnnotatedAlias ):
583
+ return True
584
+
514
585
if not hasattr (cls , '__origin__' ):
515
586
return False
516
587
origin = cls .__origin__
@@ -866,22 +937,26 @@ def repr_key(k):
866
937
def get_bindings (callable : Callable ) -> Dict [str , type ]:
867
938
"""Get bindings of injectable parameters from a callable.
868
939
869
- If the callable is not decorated with :func:`inject` an empty dictionary will
940
+ If the callable is not decorated with :func:`inject` and does not have any of its
941
+ parameters declared as injectable using :data:`Inject` an empty dictionary will
870
942
be returned. Otherwise the returned dictionary will contain a mapping
871
943
between parameter names and their types with the exception of parameters
872
- excluded from dependency injection with :func:`noninjectable`. For example::
944
+ excluded from dependency injection (either with :func:`noninjectable`, :data:`NoInject`
945
+ or only explicit injection with :data:`Inject` being used). For example::
873
946
874
947
>>> def function1(a: int) -> None:
875
948
... pass
876
949
...
877
950
>>> get_bindings(function1)
878
951
{}
952
+
879
953
>>> @inject
880
954
... def function2(a: int) -> None:
881
955
... pass
882
956
...
883
957
>>> get_bindings(function2)
884
958
{'a': int}
959
+
885
960
>>> @inject
886
961
... @noninjectable('b')
887
962
... def function3(a: int, b: str) -> None:
@@ -890,14 +965,55 @@ def get_bindings(callable: Callable) -> Dict[str, type]:
890
965
>>> get_bindings(function3)
891
966
{'a': int}
892
967
968
+ >>> # The simple case of no @inject but injection requested with Inject[...]
969
+ >>> def function4(a: Inject[int], b: str) -> None:
970
+ ... pass
971
+ ...
972
+ >>> get_bindings(function4)
973
+ {'a': int}
974
+
975
+ >>> # Using @inject with Inject is redundant but it should not break anything
976
+ >>> @inject
977
+ ... def function5(a: Inject[int], b: str) -> None:
978
+ ... pass
979
+ ...
980
+ >>> get_bindings(function5)
981
+ {'a': int, 'b': str}
982
+
983
+ >>> # We need to be able to exclude a parameter from injection with NoInject
984
+ >>> @inject
985
+ ... def function6(a: int, b: NoInject[str]) -> None:
986
+ ... pass
987
+ ...
988
+ >>> get_bindings(function6)
989
+ {'a': int}
990
+
991
+ >>> # The presence of NoInject should not trigger anything on its own
992
+ >>> def function7(a: int, b: NoInject[str]) -> None:
993
+ ... pass
994
+ ...
995
+ >>> get_bindings(function7)
996
+ {}
997
+
893
998
This function is used internally so by calling it you can learn what exactly
894
999
Injector is going to try to provide to a callable.
895
1000
"""
1001
+ look_for_explicit_bindings = False
896
1002
if not hasattr (callable , '__bindings__' ):
897
- return {}
1003
+ type_hints = get_type_hints (callable , include_extras = True )
1004
+ has_injectable_parameters = any (
1005
+ _is_specialization (v , Annotated ) and _inject_marker in v .__metadata__ for v in type_hints .values ()
1006
+ )
1007
+
1008
+ if not has_injectable_parameters :
1009
+ return {}
1010
+ else :
1011
+ look_for_explicit_bindings = True
898
1012
899
- if cast (Any , callable ).__bindings__ == 'deferred' :
900
- read_and_store_bindings (callable , _infer_injected_bindings (callable ))
1013
+ if look_for_explicit_bindings or cast (Any , callable ).__bindings__ == 'deferred' :
1014
+ read_and_store_bindings (
1015
+ callable , _infer_injected_bindings (callable , only_explicit_bindings = look_for_explicit_bindings )
1016
+ )
901
1017
noninjectables = getattr (callable , '__noninjectables__' , set ())
902
1018
return {k : v for k , v in cast (Any , callable ).__bindings__ .items () if k not in noninjectables }
903
1019
@@ -906,10 +1022,10 @@ class _BindingNotYetAvailable(Exception):
906
1022
pass
907
1023
908
1024
909
- def _infer_injected_bindings (callable ):
1025
+ def _infer_injected_bindings (callable , only_explicit_bindings : bool ):
910
1026
spec = inspect .getfullargspec (callable )
911
1027
try :
912
- bindings = get_type_hints (callable )
1028
+ bindings = get_type_hints (callable , include_extras = True )
913
1029
except NameError as e :
914
1030
raise _BindingNotYetAvailable (e )
915
1031
@@ -929,14 +1045,26 @@ def _infer_injected_bindings(callable):
929
1045
bindings .pop (spec .varkw , None )
930
1046
931
1047
for k , v in list (bindings .items ()):
1048
+ if _is_specialization (v , Annotated ):
1049
+ v , metadata = v .__origin__ , v .__metadata__
1050
+ bindings [k ] = v
1051
+ else :
1052
+ metadata = tuple ()
1053
+
1054
+ if only_explicit_bindings and _inject_marker not in metadata or _noinject_marker in metadata :
1055
+ del bindings [k ]
1056
+ break
1057
+
932
1058
if _is_specialization (v , Union ):
933
1059
# We don't treat Optional parameters in any special way at the moment.
934
1060
if TYPING353 :
935
1061
union_members = v .__args__
936
1062
else :
937
1063
union_members = v .__union_params__
938
1064
new_members = tuple (set (union_members ) - {type (None )})
939
- new_union = Union [new_members ]
1065
+ # mypy stared complaining about this line for some reason:
1066
+ # error: Variable "new_members" is not valid as a type
1067
+ new_union = Union [new_members ] # type: ignore
940
1068
# mypy complains about this construct:
941
1069
# error: The type alias is invalid in runtime context
942
1070
# See: https://github.com/python/mypy/issues/5354
@@ -1051,6 +1179,14 @@ def inject(constructor_or_class):
1051
1179
Third party libraries may, however, provide support for injecting dependencies
1052
1180
into non-constructor methods or free functions in one form or another.
1053
1181
1182
+ .. seealso::
1183
+
1184
+ Generic type :data:`Inject`
1185
+ A more explicit way to declare parameters as injectable.
1186
+
1187
+ Function :func:`get_bindings`
1188
+ A way to inspect how various injection declarations interact with each other.
1189
+
1054
1190
.. versionchanged:: 0.16.2
1055
1191
1056
1192
(Re)added support for decorating classes with @inject.
@@ -1060,7 +1196,7 @@ def inject(constructor_or_class):
1060
1196
else :
1061
1197
function = constructor_or_class
1062
1198
try :
1063
- bindings = _infer_injected_bindings (function )
1199
+ bindings = _infer_injected_bindings (function , only_explicit_bindings = False )
1064
1200
read_and_store_bindings (function , bindings )
1065
1201
except _BindingNotYetAvailable :
1066
1202
function .__bindings__ = 'deferred'
@@ -1089,6 +1225,15 @@ def noninjectable(*args):
1089
1225
each other and the order in which a function is decorated with
1090
1226
:func:`inject` and :func:`noninjectable`
1091
1227
doesn't matter.
1228
+
1229
+ .. seealso::
1230
+
1231
+ Generic type :data:`NoInject`
1232
+ A nicer way to declare parameters as noninjectable.
1233
+
1234
+ Function :func:`get_bindings`
1235
+ A way to inspect how various injection declarations interact with each other.
1236
+
1092
1237
"""
1093
1238
1094
1239
def decorator (function ):
0 commit comments