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
+ HAVE_ANNOTATED = sys . version_info >= ( 3 , 7 , 0 )
29
+
30
+ if HAVE_ANNOTATED :
31
+ # Ignoring errors here as typing_extensions stub doesn't know about those things yet
32
+ from typing_extensions import _AnnotatedAlias , Annotated , get_type_hints # type: ignore
33
+ else :
34
+ Annotated = None
35
+ from typing import get_type_hints as _get_type_hints
36
+
37
+ def get_type_hints ( what , include_extras ):
38
+ return _get_type_hints ( what )
39
+
40
40
41
41
TYPING353 = hasattr (Union [str , int ], '__origin__' )
42
42
@@ -79,6 +79,83 @@ def wrapper(*args: Any, **kwargs: Any) -> Any:
79
79
lock = threading .RLock ()
80
80
81
81
82
+ _inject_marker = object ()
83
+ _noinject_marker = object ()
84
+
85
+ if HAVE_ANNOTATED :
86
+ InjectT = TypeVar ('InjectT' )
87
+ Inject = Annotated [InjectT , _inject_marker ]
88
+ """An experimental way to declare injectable dependencies utilizing a `PEP 593`_ implementation
89
+ in `typing_extensions`. This API is likely to change when `PEP 593`_ stabilizes.
90
+
91
+ Those two declarations are equivalent::
92
+
93
+ @inject
94
+ def fun(t: SomeType) -> None:
95
+ pass
96
+
97
+ def fun(t: Inject[SomeType]) -> None:
98
+ pass
99
+
100
+ The advantage over using :func:`inject` is that if you have some noninjectable parameters
101
+ it may be easier to spot what are they. Those two are equivalent::
102
+
103
+ @inject
104
+ @noninjectable('s')
105
+ def fun(t: SomeType, s: SomeOtherType) -> None:
106
+ pass
107
+
108
+ def fun(t: Inject[SomeType], s: SomeOtherType) -> None:
109
+ pass
110
+
111
+ .. seealso::
112
+
113
+ Function :func:`get_bindings`
114
+ A way to inspect how various injection declarations interact with each other.
115
+
116
+ .. versionadded:: 0.18.0
117
+ .. note:: Requires Python 3.7+.
118
+
119
+ .. _PEP 593: https://www.python.org/dev/peps/pep-0593/
120
+ .. _typing_extensions: https://pypi.org/project/typing-extensions/
121
+ """
122
+
123
+ NoInject = Annotated [InjectT , _noinject_marker ]
124
+ """An experimental way to declare noninjectable dependencies utilizing a `PEP 593`_ implementation
125
+ in `typing_extensions`. This API is likely to change when `PEP 593`_ stabilizes.
126
+
127
+ Since :func:`inject` declares all function's parameters to be injectable there needs to be a way
128
+ to opt out of it. This has been provided by :func:`noninjectable` but `noninjectable` suffers from
129
+ two issues:
130
+
131
+ * You need to repeat the parameter name
132
+ * The declaration may be relatively distance in space from the actual parameter declaration, thus
133
+ hindering readability
134
+
135
+ `NoInject` solves both of those concerns, for example (those two declarations are equivalent)::
136
+
137
+ @inject
138
+ @noninjectable('b')
139
+ def fun(a: TypeA, b: TypeB) -> None:
140
+ pass
141
+
142
+ @inject
143
+ def fun(a: TypeA, b: NoInject[TypeB]) -> None:
144
+ pass
145
+
146
+ .. seealso::
147
+
148
+ Function :func:`get_bindings`
149
+ A way to inspect how various injection declarations interact with each other.
150
+
151
+ .. versionadded:: 0.18.0
152
+ .. note:: Requires Python 3.7+.
153
+
154
+ .. _PEP 593: https://www.python.org/dev/peps/pep-0593/
155
+ .. _typing_extensions: https://pypi.org/project/typing-extensions/
156
+ """
157
+
158
+
82
159
def reraise (original : Exception , exception : Exception , maximum_frames : int = 1 ) -> None :
83
160
prev_cls , prev , tb = sys .exc_info ()
84
161
frames = inspect .getinnerframes (cast (types .TracebackType , tb ))
@@ -511,6 +588,13 @@ def _is_specialization(cls, generic_class):
511
588
# issubclass(SomeGeneric[X], SomeGeneric) so we need some other way to
512
589
# determine whether a particular object is a generic class with type parameters
513
590
# provided. Fortunately there seems to be __origin__ attribute that's useful here.
591
+
592
+ # We need to special-case Annotated as its __origin__ behaves differently than
593
+ # other typing generic classes. See https://github.com/python/typing/pull/635
594
+ # for some details.
595
+ if HAVE_ANNOTATED and generic_class is Annotated and isinstance (cls , _AnnotatedAlias ):
596
+ return True
597
+
514
598
if not hasattr (cls , '__origin__' ):
515
599
return False
516
600
origin = cls .__origin__
@@ -866,22 +950,26 @@ def repr_key(k):
866
950
def get_bindings (callable : Callable ) -> Dict [str , type ]:
867
951
"""Get bindings of injectable parameters from a callable.
868
952
869
- If the callable is not decorated with :func:`inject` an empty dictionary will
953
+ If the callable is not decorated with :func:`inject` and does not have any of its
954
+ parameters declared as injectable using :data:`Inject` an empty dictionary will
870
955
be returned. Otherwise the returned dictionary will contain a mapping
871
956
between parameter names and their types with the exception of parameters
872
- excluded from dependency injection with :func:`noninjectable`. For example::
957
+ excluded from dependency injection (either with :func:`noninjectable`, :data:`NoInject`
958
+ or only explicit injection with :data:`Inject` being used). For example::
873
959
874
960
>>> def function1(a: int) -> None:
875
961
... pass
876
962
...
877
963
>>> get_bindings(function1)
878
964
{}
965
+
879
966
>>> @inject
880
967
... def function2(a: int) -> None:
881
968
... pass
882
969
...
883
970
>>> get_bindings(function2)
884
971
{'a': int}
972
+
885
973
>>> @inject
886
974
... @noninjectable('b')
887
975
... def function3(a: int, b: str) -> None:
@@ -890,14 +978,55 @@ def get_bindings(callable: Callable) -> Dict[str, type]:
890
978
>>> get_bindings(function3)
891
979
{'a': int}
892
980
981
+ >>> # The simple case of no @inject but injection requested with Inject[...]
982
+ >>> def function4(a: Inject[int], b: str) -> None:
983
+ ... pass
984
+ ...
985
+ >>> get_bindings(function4)
986
+ {'a': int}
987
+
988
+ >>> # Using @inject with Inject is redundant but it should not break anything
989
+ >>> @inject
990
+ ... def function5(a: Inject[int], b: str) -> None:
991
+ ... pass
992
+ ...
993
+ >>> get_bindings(function5)
994
+ {'a': int, 'b': str}
995
+
996
+ >>> # We need to be able to exclude a parameter from injection with NoInject
997
+ >>> @inject
998
+ ... def function6(a: int, b: NoInject[str]) -> None:
999
+ ... pass
1000
+ ...
1001
+ >>> get_bindings(function6)
1002
+ {'a': int}
1003
+
1004
+ >>> # The presence of NoInject should not trigger anything on its own
1005
+ >>> def function7(a: int, b: NoInject[str]) -> None:
1006
+ ... pass
1007
+ ...
1008
+ >>> get_bindings(function7)
1009
+ {}
1010
+
893
1011
This function is used internally so by calling it you can learn what exactly
894
1012
Injector is going to try to provide to a callable.
895
1013
"""
1014
+ look_for_explicit_bindings = False
896
1015
if not hasattr (callable , '__bindings__' ):
897
- return {}
1016
+ type_hints = get_type_hints (callable , include_extras = True )
1017
+ has_injectable_parameters = any (
1018
+ _is_specialization (v , Annotated ) and _inject_marker in v .__metadata__ for v in type_hints .values ()
1019
+ )
1020
+
1021
+ if not has_injectable_parameters :
1022
+ return {}
1023
+ else :
1024
+ look_for_explicit_bindings = True
898
1025
899
- if cast (Any , callable ).__bindings__ == 'deferred' :
900
- read_and_store_bindings (callable , _infer_injected_bindings (callable ))
1026
+ if look_for_explicit_bindings or cast (Any , callable ).__bindings__ == 'deferred' :
1027
+ read_and_store_bindings (
1028
+ callable , _infer_injected_bindings (callable , only_explicit_bindings = look_for_explicit_bindings )
1029
+ )
901
1030
noninjectables = getattr (callable , '__noninjectables__' , set ())
902
1031
return {k : v for k , v in cast (Any , callable ).__bindings__ .items () if k not in noninjectables }
903
1032
@@ -906,10 +1035,10 @@ class _BindingNotYetAvailable(Exception):
906
1035
pass
907
1036
908
1037
909
- def _infer_injected_bindings (callable ):
1038
+ def _infer_injected_bindings (callable , only_explicit_bindings : bool ):
910
1039
spec = inspect .getfullargspec (callable )
911
1040
try :
912
- bindings = get_type_hints (callable )
1041
+ bindings = get_type_hints (callable , include_extras = True )
913
1042
except NameError as e :
914
1043
raise _BindingNotYetAvailable (e )
915
1044
@@ -929,14 +1058,26 @@ def _infer_injected_bindings(callable):
929
1058
bindings .pop (spec .varkw , None )
930
1059
931
1060
for k , v in list (bindings .items ()):
1061
+ if _is_specialization (v , Annotated ):
1062
+ v , metadata = v .__origin__ , v .__metadata__
1063
+ bindings [k ] = v
1064
+ else :
1065
+ metadata = tuple ()
1066
+
1067
+ if only_explicit_bindings and _inject_marker not in metadata or _noinject_marker in metadata :
1068
+ del bindings [k ]
1069
+ break
1070
+
932
1071
if _is_specialization (v , Union ):
933
1072
# We don't treat Optional parameters in any special way at the moment.
934
1073
if TYPING353 :
935
1074
union_members = v .__args__
936
1075
else :
937
1076
union_members = v .__union_params__
938
1077
new_members = tuple (set (union_members ) - {type (None )})
939
- new_union = Union [new_members ]
1078
+ # mypy stared complaining about this line for some reason:
1079
+ # error: Variable "new_members" is not valid as a type
1080
+ new_union = Union [new_members ] # type: ignore
940
1081
# mypy complains about this construct:
941
1082
# error: The type alias is invalid in runtime context
942
1083
# See: https://github.com/python/mypy/issues/5354
@@ -1051,6 +1192,14 @@ def inject(constructor_or_class):
1051
1192
Third party libraries may, however, provide support for injecting dependencies
1052
1193
into non-constructor methods or free functions in one form or another.
1053
1194
1195
+ .. seealso::
1196
+
1197
+ Generic type :data:`Inject`
1198
+ A more explicit way to declare parameters as injectable.
1199
+
1200
+ Function :func:`get_bindings`
1201
+ A way to inspect how various injection declarations interact with each other.
1202
+
1054
1203
.. versionchanged:: 0.16.2
1055
1204
1056
1205
(Re)added support for decorating classes with @inject.
@@ -1060,7 +1209,7 @@ def inject(constructor_or_class):
1060
1209
else :
1061
1210
function = constructor_or_class
1062
1211
try :
1063
- bindings = _infer_injected_bindings (function )
1212
+ bindings = _infer_injected_bindings (function , only_explicit_bindings = False )
1064
1213
read_and_store_bindings (function , bindings )
1065
1214
except _BindingNotYetAvailable :
1066
1215
function .__bindings__ = 'deferred'
@@ -1089,6 +1238,15 @@ def noninjectable(*args):
1089
1238
each other and the order in which a function is decorated with
1090
1239
:func:`inject` and :func:`noninjectable`
1091
1240
doesn't matter.
1241
+
1242
+ .. seealso::
1243
+
1244
+ Generic type :data:`NoInject`
1245
+ A nicer way to declare parameters as noninjectable.
1246
+
1247
+ Function :func:`get_bindings`
1248
+ A way to inspect how various injection declarations interact with each other.
1249
+
1092
1250
"""
1093
1251
1094
1252
def decorator (function ):
0 commit comments