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