fast-depends 1.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,20 @@
1
+ dist
2
+ __pycache__
3
+ *.lock
4
+ .vscode
5
+ .pypirc
6
+ venv
7
+ app
8
+ serve.py
9
+ test.py
10
+ poetry.lock
11
+ docker-compose.yaml
12
+ .pytest_cache
13
+ poetry.lock
14
+ .coverage*
15
+ htmlcov
16
+ wtf
17
+ .ruff_cache
18
+ .mypy_cache
19
+ coverage.json
20
+ site
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2023 Pastukhov Nikita
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
@@ -0,0 +1,135 @@
1
+ Metadata-Version: 2.1
2
+ Name: fast-depends
3
+ Version: 1.0.0
4
+ Summary: FastDepends - extracted and cleared from HTTP domain Fastapi Dependency Injection System
5
+ Project-URL: Homepage, https://lancetnik.github.io/FastDI/
6
+ Project-URL: Documentation, https://lancetnik.github.io/FastDI/
7
+ Project-URL: Tracker, https://github.com/Lancetnik/FastDI/issues
8
+ Project-URL: Source, https://github.com/Lancetnik/FastDI
9
+ Author-email: Pastukhov Nikita <diementros@yandex.ru>
10
+ License-File: LICENSE
11
+ Keywords: dependency injection,fastapi
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Framework :: Pydantic
14
+ Classifier: Framework :: Pydantic :: 1
15
+ Classifier: Intended Audience :: Developers
16
+ Classifier: Intended Audience :: Information Technology
17
+ Classifier: License :: OSI Approved :: MIT License
18
+ Classifier: Operating System :: OS Independent
19
+ Classifier: Programming Language :: Python
20
+ Classifier: Programming Language :: Python :: 3
21
+ Classifier: Programming Language :: Python :: 3 :: Only
22
+ Classifier: Programming Language :: Python :: 3.7
23
+ Classifier: Programming Language :: Python :: 3.8
24
+ Classifier: Programming Language :: Python :: 3.9
25
+ Classifier: Programming Language :: Python :: 3.10
26
+ Classifier: Programming Language :: Python :: 3.11
27
+ Classifier: Programming Language :: Python :: Implementation :: CPython
28
+ Classifier: Topic :: Software Development
29
+ Classifier: Topic :: Software Development :: Libraries
30
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
31
+ Classifier: Typing :: Typed
32
+ Requires-Python: >=3.7
33
+ Requires-Dist: anyio
34
+ Requires-Dist: pydantic>=1.8
35
+ Provides-Extra: dev
36
+ Requires-Dist: black>=23.3.0; extra == 'dev'
37
+ Requires-Dist: fastdepends[doc]; extra == 'dev'
38
+ Requires-Dist: fastdepends[test]; extra == 'dev'
39
+ Requires-Dist: isort>=5; extra == 'dev'
40
+ Requires-Dist: mypy>=1.1; extra == 'dev'
41
+ Requires-Dist: ruff>=0.0.260; extra == 'dev'
42
+ Provides-Extra: doc
43
+ Requires-Dist: mdx-include<2.0.0,>=1.4.1; extra == 'doc'
44
+ Requires-Dist: mkdocs-markdownextradata-plugin<0.3.0,>=0.1.7; extra == 'doc'
45
+ Requires-Dist: mkdocs-material<9.0.0,>=8.1.4; extra == 'doc'
46
+ Requires-Dist: mkdocstrings[python]>=0.18; extra == 'doc'
47
+ Provides-Extra: test
48
+ Requires-Dist: asyncmock; python_version < '3.8' and extra == 'test'
49
+ Requires-Dist: coverage[toml]>=7.2; extra == 'test'
50
+ Requires-Dist: pytest-asyncio>=0.21; extra == 'test'
51
+ Requires-Dist: pytest-xdist[psutil]; extra == 'test'
52
+ Requires-Dist: pytest>=7; extra == 'test'
53
+ Description-Content-Type: text/markdown
54
+
55
+ # FastDI
56
+
57
+ ---
58
+
59
+ Documentation: https://lancetnik.github.io/FastDI/
60
+
61
+ ---
62
+
63
+ FastDI - extracted and cleared from HTTP domain logic Fastapi Dependency Injection System.
64
+ This is a little library, providing you ability to use lovely Fastapi interfaces at your own
65
+ projects or tools.
66
+
67
+ Thanks to [*fastapi*](https://fastapi.tiangolo.com/) and [*pytest*](https://docs.pytest.org/en/7.3.x/) projects for this
68
+ greate functional. This package just a little change Fasapi sources to provide DI functionality pyre-python way.
69
+
70
+ ## Usage
71
+
72
+ There is no way to make Dependency Injection easier
73
+
74
+ You can use this library without any frameworks at **sync** and **async** code both
75
+
76
+ ### Async code
77
+ ```python
78
+ import asyncio
79
+
80
+ from fastdi import inject, Depends
81
+
82
+ async def dependency(a: int) -> int:
83
+ return a
84
+
85
+ @inject
86
+ async def main(
87
+ a: int,
88
+ b: int,
89
+ c: int = Depends(dependency)
90
+ ) -> float:
91
+ return a + b + c
92
+
93
+ assert asyncio.run(main("1", 2)) == 4.0
94
+ ```
95
+
96
+ ### Sync code
97
+ ```python
98
+ from fastdi import inject, Depends
99
+
100
+ def dependency(a: int) -> int:
101
+ return a
102
+
103
+ @inject
104
+ def main(
105
+ a: int,
106
+ b: int,
107
+ c: int = Depends(dependency)
108
+ ) -> float:
109
+ return a + b + c
110
+
111
+ assert main("1", 2) == 4.0
112
+ ```
113
+
114
+ `@inject` decorator playing multiple roles at the same time:
115
+
116
+ * resolve *Depends* classes
117
+ * cast types according python annotation
118
+ * validate incoming parameters using *pydantic*
119
+
120
+ ---
121
+
122
+ ### Features
123
+ Syncronous code is fully supported at this package: without any `async_to_sync`, `run_sync`, `syncify` or any another tricks.
124
+
125
+ Also, *FastDI* casts function return the same way, it can be very felpfull to build your own tools.
126
+
127
+ There is two main defferences from native Fastapi DI System.
128
+
129
+ ---
130
+
131
+ ### Note
132
+ Library was build by actual **0.95.0 FastAPI** version.
133
+
134
+ If we'll be too far behind, please, contact [me](mailto:diementros@yandex.ru)
135
+ or contubute yourself. Really appreciate your help.
@@ -0,0 +1,81 @@
1
+ # FastDI
2
+
3
+ ---
4
+
5
+ Documentation: https://lancetnik.github.io/FastDI/
6
+
7
+ ---
8
+
9
+ FastDI - extracted and cleared from HTTP domain logic Fastapi Dependency Injection System.
10
+ This is a little library, providing you ability to use lovely Fastapi interfaces at your own
11
+ projects or tools.
12
+
13
+ Thanks to [*fastapi*](https://fastapi.tiangolo.com/) and [*pytest*](https://docs.pytest.org/en/7.3.x/) projects for this
14
+ greate functional. This package just a little change Fasapi sources to provide DI functionality pyre-python way.
15
+
16
+ ## Usage
17
+
18
+ There is no way to make Dependency Injection easier
19
+
20
+ You can use this library without any frameworks at **sync** and **async** code both
21
+
22
+ ### Async code
23
+ ```python
24
+ import asyncio
25
+
26
+ from fastdi import inject, Depends
27
+
28
+ async def dependency(a: int) -> int:
29
+ return a
30
+
31
+ @inject
32
+ async def main(
33
+ a: int,
34
+ b: int,
35
+ c: int = Depends(dependency)
36
+ ) -> float:
37
+ return a + b + c
38
+
39
+ assert asyncio.run(main("1", 2)) == 4.0
40
+ ```
41
+
42
+ ### Sync code
43
+ ```python
44
+ from fastdi import inject, Depends
45
+
46
+ def dependency(a: int) -> int:
47
+ return a
48
+
49
+ @inject
50
+ def main(
51
+ a: int,
52
+ b: int,
53
+ c: int = Depends(dependency)
54
+ ) -> float:
55
+ return a + b + c
56
+
57
+ assert main("1", 2) == 4.0
58
+ ```
59
+
60
+ `@inject` decorator playing multiple roles at the same time:
61
+
62
+ * resolve *Depends* classes
63
+ * cast types according python annotation
64
+ * validate incoming parameters using *pydantic*
65
+
66
+ ---
67
+
68
+ ### Features
69
+ Syncronous code is fully supported at this package: without any `async_to_sync`, `run_sync`, `syncify` or any another tricks.
70
+
71
+ Also, *FastDI* casts function return the same way, it can be very felpfull to build your own tools.
72
+
73
+ There is two main defferences from native Fastapi DI System.
74
+
75
+ ---
76
+
77
+ ### Note
78
+ Library was build by actual **0.95.0 FastAPI** version.
79
+
80
+ If we'll be too far behind, please, contact [me](mailto:diementros@yandex.ru)
81
+ or contubute yourself. Really appreciate your help.
@@ -0,0 +1,3 @@
1
+ """FastDepends - extracted and cleared from HTTP domain Fastapi Dependency Injection System"""
2
+
3
+ __version__ = "1.0.0"
@@ -0,0 +1,8 @@
1
+ from fast_depends.provider import dependency_provider
2
+ from fast_depends.usage import Depends, inject
3
+
4
+ __all__ = (
5
+ "inject",
6
+ "Depends",
7
+ "dependency_provider",
8
+ )
@@ -0,0 +1,250 @@
1
+ import inspect
2
+ from typing import Any, Dict, Optional, Tuple, Type, Union
3
+
4
+ from pydantic import BaseConfig
5
+ from pydantic.class_validators import Validator
6
+ from pydantic.fields import (
7
+ SHAPE_FROZENSET,
8
+ SHAPE_LIST,
9
+ SHAPE_SEQUENCE,
10
+ SHAPE_SET,
11
+ SHAPE_TUPLE,
12
+ SHAPE_TUPLE_ELLIPSIS,
13
+ FieldInfo,
14
+ ModelField,
15
+ Required,
16
+ Undefined,
17
+ UndefinedType,
18
+ )
19
+ from pydantic.schema import get_annotation_from_field_info
20
+ from pydantic.typing import ForwardRef, evaluate_forwardref, get_args, get_origin
21
+ from typing_extensions import Annotated
22
+
23
+ from fast_depends import model
24
+ from fast_depends.types import AnyCallable, AnyDict
25
+
26
+ sequence_shapes = {
27
+ SHAPE_LIST,
28
+ SHAPE_SET,
29
+ SHAPE_FROZENSET,
30
+ SHAPE_TUPLE,
31
+ SHAPE_SEQUENCE,
32
+ SHAPE_TUPLE_ELLIPSIS,
33
+ }
34
+ sequence_types = (list, set, tuple)
35
+
36
+
37
+ def get_dependant(
38
+ *,
39
+ path: str,
40
+ call: AnyCallable,
41
+ name: Optional[str] = None,
42
+ use_cache: bool = True,
43
+ ) -> model.Dependant:
44
+ dependant = model.Dependant(
45
+ call=call,
46
+ path=path,
47
+ name=name,
48
+ use_cache=use_cache,
49
+ return_field=None,
50
+ )
51
+
52
+ endpoint_signature = get_typed_signature(call)
53
+ signature_params = endpoint_signature.parameters
54
+
55
+ for param in signature_params.values():
56
+ depends, param_field = analyze_param(
57
+ param_name=param.name,
58
+ annotation=param.annotation,
59
+ default=param.default,
60
+ )
61
+
62
+ if param.name == model.RETURN_FIELD:
63
+ dependant.return_field = param_field
64
+ continue
65
+
66
+ if depends is not None:
67
+ sub_dependant = get_param_sub_dependant(
68
+ param_name=param.name,
69
+ depends=depends,
70
+ path=path,
71
+ )
72
+ dependant.dependencies.append(sub_dependant)
73
+
74
+ dependant.params.append(param_field)
75
+
76
+ return dependant
77
+
78
+
79
+ def analyze_param(
80
+ *,
81
+ param_name: str,
82
+ annotation: Any,
83
+ default: Any,
84
+ ) -> Tuple[Any, Optional[model.Depends], Optional[ModelField]]:
85
+ depends = None
86
+ field_info = None
87
+
88
+ if (
89
+ annotation is not inspect.Signature.empty
90
+ and get_origin(annotation) is Annotated # type: ignore[comparison-overlap]
91
+ ):
92
+ annotated_args = get_args(annotation)
93
+ custom_annotations = [
94
+ arg
95
+ for arg in annotated_args[1:]
96
+ if isinstance(arg, (FieldInfo, model.Depends))
97
+ ]
98
+
99
+ custom_annotations = next(iter(custom_annotations), None)
100
+ if isinstance(custom_annotations, FieldInfo):
101
+ field_info = custom_annotations
102
+ assert field_info.default is Undefined or field_info.default is Required, (
103
+ f"`{field_info.__class__.__name__}` default value cannot be set in"
104
+ f" `Annotated` for {param_name!r}. Set the default value with `=` instead."
105
+ )
106
+ field_info.default = Required
107
+
108
+ elif isinstance(custom_annotations, model.Depends): # pragma: no branch
109
+ depends = custom_annotations
110
+
111
+ if isinstance(default, model.Depends):
112
+ assert depends is None, (
113
+ "Cannot specify `Depends` in `Annotated` and default value"
114
+ f" together for {param_name!r}"
115
+ )
116
+ assert field_info is None, (
117
+ "Cannot specify a annotation in `Annotated` and `Depends` as a"
118
+ f" default value together for {param_name!r}"
119
+ )
120
+ depends = default
121
+
122
+ elif isinstance(default, FieldInfo):
123
+ assert field_info is None, (
124
+ "Cannot specify annotations in `Annotated` and default value"
125
+ f" together for {param_name!r}"
126
+ )
127
+ field_info = default
128
+
129
+ if depends is not None:
130
+ field = None
131
+
132
+ if field_info is not None:
133
+ annotation = get_annotation_from_field_info(
134
+ annotation if annotation is not inspect.Signature.empty else Any,
135
+ field_info,
136
+ param_name,
137
+ )
138
+ else:
139
+ field_info = FieldInfo(default=default)
140
+
141
+ alias = field_info.alias or param_name
142
+
143
+ field = create_response_field(
144
+ name=param_name,
145
+ type_=annotation,
146
+ default=field_info.default if depends is None else None,
147
+ alias=alias,
148
+ required=field_info.default in (Required, Undefined, inspect._empty),
149
+ field_info=field_info,
150
+ )
151
+
152
+ return depends, field
153
+
154
+
155
+ def get_typed_signature(call: AnyCallable) -> inspect.Signature:
156
+ signature = inspect.signature(call)
157
+ globalns = getattr(call, "__globals__", {})
158
+ typed_params = [
159
+ inspect.Parameter(
160
+ name=param.name,
161
+ kind=param.kind,
162
+ default=param.default,
163
+ annotation=get_typed_annotation(param.annotation, globalns),
164
+ )
165
+ for param in signature.parameters.values()
166
+ ]
167
+
168
+ if signature.return_annotation is not signature.empty:
169
+ typed_params.append(
170
+ inspect.Parameter(
171
+ name=model.RETURN_FIELD,
172
+ kind=inspect._KEYWORD_ONLY,
173
+ annotation=get_typed_annotation(signature.return_annotation, globalns),
174
+ )
175
+ )
176
+ typed_signature = inspect.Signature(typed_params)
177
+ return typed_signature
178
+
179
+
180
+ def get_typed_annotation(annotation: Any, globalns: AnyDict) -> Any:
181
+ if isinstance(annotation, str):
182
+ try:
183
+ annotation = ForwardRef(annotation)
184
+ annotation = evaluate_forwardref(annotation, globalns, globalns)
185
+ except Exception:
186
+ raise ValueError( # noqa: B904
187
+ f"Invalid filed annotation! Hint: check that {annotation} is a valid pydantic field type"
188
+ )
189
+ return annotation
190
+
191
+
192
+ def get_param_sub_dependant(
193
+ *,
194
+ param_name: str,
195
+ depends: model.Depends,
196
+ path: str,
197
+ ) -> model.Dependant:
198
+ assert depends.dependency
199
+ return get_sub_dependant(
200
+ depends=depends,
201
+ dependency=depends.dependency,
202
+ path=path,
203
+ name=param_name,
204
+ )
205
+
206
+
207
+ def get_sub_dependant(
208
+ *,
209
+ depends: model.Depends,
210
+ dependency: AnyCallable,
211
+ path: str,
212
+ name: Optional[str] = None,
213
+ ) -> model.Dependant:
214
+ sub_dependant = get_dependant(
215
+ path=path,
216
+ call=dependency,
217
+ name=name,
218
+ use_cache=depends.use_cache,
219
+ )
220
+ return sub_dependant
221
+
222
+
223
+ def create_response_field(
224
+ name: str,
225
+ type_: Type[Any],
226
+ class_validators: Optional[Dict[str, Validator]] = None,
227
+ default: Optional[Any] = None,
228
+ required: Union[bool, UndefinedType] = True,
229
+ model_config: Type[BaseConfig] = BaseConfig,
230
+ field_info: Optional[FieldInfo] = None,
231
+ alias: Optional[str] = None,
232
+ ) -> ModelField:
233
+ """
234
+ Create a new response field. Raises if type_ is invalid.
235
+ """
236
+ try:
237
+ return ModelField(
238
+ name=name,
239
+ type_=type_ if type_ is not inspect._empty else Any,
240
+ class_validators=class_validators or {},
241
+ default=default,
242
+ required=required,
243
+ model_config=model_config,
244
+ alias=alias,
245
+ field_info=field_info or FieldInfo(),
246
+ )
247
+ except RuntimeError: # pragma: no cover
248
+ raise ValueError( # noqa: B904
249
+ f"Invalid args for response field! Hint: check that {type_} is a valid pydantic field type"
250
+ )
@@ -0,0 +1,308 @@
1
+ import asyncio
2
+ import functools
3
+ import inspect
4
+ from contextlib import AsyncExitStack, ExitStack, asynccontextmanager, contextmanager
5
+ from copy import deepcopy
6
+ from typing import (
7
+ Any,
8
+ AsyncGenerator,
9
+ Callable,
10
+ ContextManager,
11
+ Dict,
12
+ List,
13
+ Mapping,
14
+ Optional,
15
+ Sequence,
16
+ Tuple,
17
+ TypeVar,
18
+ cast,
19
+ )
20
+
21
+ import anyio
22
+ from pydantic.error_wrappers import ErrorWrapper
23
+ from pydantic.errors import MissingError
24
+ from pydantic.fields import ModelField
25
+
26
+ from fast_depends.construct import get_dependant
27
+ from fast_depends.model import Dependant
28
+ from fast_depends.types import AnyCallable, AnyDict, P
29
+
30
+ T = TypeVar("T")
31
+
32
+
33
+ async def solve_dependencies_async(
34
+ *,
35
+ dependant: Dependant,
36
+ stack: AsyncExitStack,
37
+ body: Optional[AnyDict] = None,
38
+ dependency_overrides_provider: Optional[Any] = None,
39
+ dependency_cache: Optional[Dict[Tuple[AnyCallable, Tuple[str]], Any]] = None,
40
+ ) -> Tuple[
41
+ Dict[str, Any],
42
+ List[ErrorWrapper],
43
+ Dict[Tuple[AnyCallable, Tuple[str]], Any],
44
+ ]:
45
+ errors: List[ErrorWrapper] = []
46
+
47
+ dependency_cache = dependency_cache or {}
48
+
49
+ sub_dependant: Dependant
50
+ for sub_dependant in dependant.dependencies:
51
+ sub_dependant.call = cast(AnyCallable, sub_dependant.call)
52
+ sub_dependant.cache_key = cast(
53
+ Tuple[AnyCallable, Tuple[str]], sub_dependant.cache_key
54
+ )
55
+ call = sub_dependant.call
56
+ use_sub_dependant = sub_dependant
57
+ if (
58
+ dependency_overrides_provider
59
+ and dependency_overrides_provider.dependency_overrides
60
+ ):
61
+ call = getattr(
62
+ dependency_overrides_provider, "dependency_overrides", {}
63
+ ).get(sub_dependant.call)
64
+ if call is not None: # pragma: no branch
65
+ use_sub_dependant = get_dependant(
66
+ path=sub_dependant.path,
67
+ call=call,
68
+ name=sub_dependant.name,
69
+ )
70
+
71
+ solved_result = await solve_dependencies_async(
72
+ dependant=use_sub_dependant,
73
+ body=body,
74
+ dependency_overrides_provider=dependency_overrides_provider,
75
+ dependency_cache=dependency_cache,
76
+ stack=stack,
77
+ )
78
+ (
79
+ sub_values,
80
+ sub_errors,
81
+ sub_dependency_cache,
82
+ ) = solved_result
83
+
84
+ dependency_cache.update(sub_dependency_cache)
85
+
86
+ if sub_errors:
87
+ errors.extend(sub_errors)
88
+ continue
89
+
90
+ if (
91
+ use_sub_dependant.use_cache
92
+ and use_sub_dependant.cache_key in dependency_cache
93
+ ):
94
+ solved = dependency_cache[use_sub_dependant.cache_key]
95
+ elif is_gen_callable(call) or is_async_gen_callable(call):
96
+ solved, sub_errors = use_sub_dependant.cast_response(
97
+ await solve_generator_async(
98
+ call=call, stack=stack, sub_values=sub_values
99
+ )
100
+ )
101
+ else:
102
+ solved, sub_errors = use_sub_dependant.cast_response(
103
+ await run_async(dependant=use_sub_dependant, values=sub_values)
104
+ )
105
+
106
+ if sub_errors:
107
+ errors.append(sub_errors)
108
+ continue
109
+
110
+ if use_sub_dependant.name is not None: # pragma: no branch
111
+ body[use_sub_dependant.name] = solved
112
+
113
+ if use_sub_dependant.cache_key not in dependency_cache:
114
+ dependency_cache[use_sub_dependant.cache_key] = solved
115
+
116
+ params, main_errors = params_to_args(dependant.params, body or {})
117
+ errors.extend(main_errors)
118
+ return params, errors, dependency_cache
119
+
120
+
121
+ def solve_dependencies_sync(
122
+ *,
123
+ dependant: Dependant,
124
+ stack: ExitStack,
125
+ body: Optional[AnyDict] = None,
126
+ dependency_overrides_provider: Optional[Any] = None,
127
+ dependency_cache: Optional[Dict[Tuple[AnyCallable, Tuple[str]], Any]] = None,
128
+ ) -> Tuple[
129
+ Dict[str, Any],
130
+ List[ErrorWrapper],
131
+ Dict[Tuple[AnyCallable, Tuple[str]], Any],
132
+ ]:
133
+ assert not is_coroutine_callable(dependant.call) and not is_async_gen_callable(
134
+ dependant.call
135
+ ), f"You can't call async `{dependant.call.__name__}` at sync code"
136
+
137
+ errors: List[ErrorWrapper] = []
138
+
139
+ dependency_cache = dependency_cache or {}
140
+
141
+ sub_dependant: Dependant
142
+ for sub_dependant in dependant.dependencies:
143
+ sub_dependant.call = cast(AnyCallable, sub_dependant.call)
144
+ sub_dependant.cache_key = cast(
145
+ Tuple[AnyCallable, Tuple[str]], sub_dependant.cache_key
146
+ )
147
+ call = sub_dependant.call
148
+ use_sub_dependant = sub_dependant
149
+ if (
150
+ dependency_overrides_provider
151
+ and dependency_overrides_provider.dependency_overrides
152
+ ):
153
+ call = getattr(
154
+ dependency_overrides_provider, "dependency_overrides", {}
155
+ ).get(sub_dependant.call)
156
+ if call is not None: # pragma: no branch
157
+ use_sub_dependant = get_dependant(
158
+ path=sub_dependant.path,
159
+ call=call,
160
+ name=sub_dependant.name,
161
+ )
162
+
163
+ solved_result = solve_dependencies_sync(
164
+ dependant=use_sub_dependant,
165
+ body=body,
166
+ stack=stack,
167
+ dependency_overrides_provider=dependency_overrides_provider,
168
+ dependency_cache=dependency_cache,
169
+ )
170
+ (
171
+ sub_values,
172
+ sub_errors,
173
+ sub_dependency_cache,
174
+ ) = solved_result
175
+
176
+ dependency_cache.update(sub_dependency_cache)
177
+
178
+ if sub_errors:
179
+ errors.extend(sub_errors)
180
+ continue
181
+
182
+ if (
183
+ use_sub_dependant.use_cache
184
+ and use_sub_dependant.cache_key in dependency_cache
185
+ ):
186
+ solved = dependency_cache[sub_dependant.cache_key]
187
+ elif is_gen_callable(call):
188
+ solved, sub_errors = use_sub_dependant.cast_response(
189
+ solve_generator_sync(call=call, stack=stack, sub_values=sub_values)
190
+ )
191
+ else:
192
+ solved, sub_errors = use_sub_dependant.cast_response(call(**sub_values))
193
+
194
+ if sub_errors:
195
+ errors.append(sub_errors)
196
+ continue
197
+
198
+ if use_sub_dependant.name is not None: # pragma: no branch
199
+ body[sub_dependant.name] = solved
200
+
201
+ if use_sub_dependant.cache_key not in dependency_cache:
202
+ dependency_cache[use_sub_dependant.cache_key] = solved
203
+
204
+ params, main_errors = params_to_args(dependant.params, body or {})
205
+ errors.extend(main_errors)
206
+ return params, errors, dependency_cache
207
+
208
+
209
+ def is_async_gen_callable(call: Callable[..., Any]) -> bool:
210
+ if inspect.isasyncgenfunction(call):
211
+ return True
212
+ dunder_call = getattr(call, "__call__", None) # noqa: B004
213
+ return inspect.isasyncgenfunction(dunder_call)
214
+
215
+
216
+ def is_gen_callable(call: Callable[..., Any]) -> bool:
217
+ if inspect.isgeneratorfunction(call):
218
+ return True
219
+ dunder_call = getattr(call, "__call__", None) # noqa: B004
220
+ return inspect.isgeneratorfunction(dunder_call)
221
+
222
+
223
+ def is_coroutine_callable(call: AnyCallable) -> bool:
224
+ if inspect.isroutine(call):
225
+ return inspect.iscoroutinefunction(call)
226
+ if inspect.isclass(call):
227
+ return False
228
+ call = getattr(call, "__call__", None) # noqa: B004
229
+ return inspect.iscoroutinefunction(call)
230
+
231
+
232
+ async def solve_generator_async(
233
+ *, call: Callable[..., Any], stack: AsyncExitStack, sub_values: Dict[str, Any]
234
+ ) -> Any:
235
+ if is_gen_callable(call):
236
+ cm = contextmanager_in_threadpool(contextmanager(call)(**sub_values))
237
+ elif is_async_gen_callable(call): # pragma: no branch
238
+ cm = asynccontextmanager(call)(**sub_values)
239
+ return await stack.enter_async_context(cm)
240
+
241
+
242
+ def solve_generator_sync(
243
+ *, call: Callable[..., Any], stack: ExitStack, sub_values: Dict[str, Any]
244
+ ) -> Any:
245
+ cm = contextmanager(call)(**sub_values)
246
+ return stack.enter_context(cm)
247
+
248
+
249
+ async def run_async(*, dependant: Dependant, values: AnyDict) -> Any:
250
+ assert dependant.call is not None, "dependant.call must be a function"
251
+ if asyncio.iscoroutinefunction(dependant.call):
252
+ return await dependant.call(**values)
253
+ else:
254
+ return await run_in_threadpool(dependant.call, **values)
255
+
256
+
257
+ async def run_in_threadpool(
258
+ func: Callable[P, T], *args: P.args, **kwargs: P.kwargs
259
+ ) -> T:
260
+ if kwargs: # pragma: no cover
261
+ func = functools.partial(func, **kwargs)
262
+ return await anyio.to_thread.run_sync(func, *args)
263
+
264
+
265
+ @asynccontextmanager
266
+ async def contextmanager_in_threadpool(
267
+ cm: ContextManager[T],
268
+ ) -> AsyncGenerator[T, None]:
269
+ exit_limiter = anyio.CapacityLimiter(1)
270
+ try:
271
+ yield await run_in_threadpool(cm.__enter__)
272
+ except Exception as e:
273
+ ok = bool(
274
+ await anyio.to_thread.run_sync(
275
+ cm.__exit__, type(e), e, None, limiter=exit_limiter
276
+ )
277
+ )
278
+ if not ok: # pragma: no branch
279
+ raise e
280
+ else:
281
+ await anyio.to_thread.run_sync(
282
+ cm.__exit__, None, None, None, limiter=exit_limiter
283
+ )
284
+
285
+
286
+ def params_to_args(
287
+ required_params: Sequence[ModelField],
288
+ received_params: Mapping[str, Any],
289
+ ) -> Tuple[AnyDict, List[ErrorWrapper]]:
290
+ values: AnyDict = {}
291
+ errors: List[ErrorWrapper] = []
292
+ for field in required_params:
293
+ value = received_params.get(field.alias)
294
+ if value is None:
295
+ if field.required:
296
+ errors.append(ErrorWrapper(MissingError(), loc=(field.alias,)))
297
+ else:
298
+ values[field.name] = deepcopy(field.default)
299
+ continue
300
+
301
+ v_, errors_ = field.validate(value, values, loc=(field.alias,))
302
+ if isinstance(errors_, ErrorWrapper):
303
+ errors.append(errors_)
304
+ elif isinstance(errors_, list): # pragma: no cover
305
+ errors.extend(errors_)
306
+ else:
307
+ values[field.name] = v_
308
+ return values, errors
@@ -0,0 +1,51 @@
1
+ from typing import Any, List, Optional, Tuple
2
+
3
+ from pydantic.error_wrappers import ErrorList
4
+ from pydantic.fields import ModelField
5
+
6
+ from fast_depends.types import AnyCallable
7
+
8
+ RETURN_FIELD = "custom_return"
9
+
10
+
11
+ class Dependant:
12
+ def __init__(
13
+ self,
14
+ *,
15
+ call: Optional[AnyCallable] = None,
16
+ params: Optional[List[ModelField]] = None,
17
+ return_field: Optional[ModelField] = None,
18
+ dependencies: Optional[List["Dependant"]] = None,
19
+ use_cache: bool = True,
20
+ path: Optional[str] = None,
21
+ name: Optional[str] = None,
22
+ ) -> None:
23
+ self.params = params or []
24
+ self.return_field = return_field
25
+ self.dependencies = dependencies or []
26
+ self.call = call
27
+ self.use_cache = use_cache
28
+ # Parent argument name at subdependency
29
+ self.name = name
30
+ # Store the path to be able to re-generate a dependable from it in overrides
31
+ self.path = path
32
+ # Save the cache key at creation to optimize performance
33
+ self.cache_key = (self.call,)
34
+
35
+ def cast_response(self, response: Any) -> Tuple[Optional[Any], Optional[ErrorList]]:
36
+ if self.return_field is None:
37
+ return response, []
38
+ return self.return_field.validate(response, {}, loc=RETURN_FIELD)
39
+
40
+
41
+ class Depends:
42
+ def __init__(
43
+ self, dependency: Optional[AnyCallable] = None, *, use_cache: bool = True
44
+ ) -> None:
45
+ self.dependency = dependency
46
+ self.use_cache = use_cache
47
+
48
+ def __repr__(self) -> str:
49
+ attr = getattr(self.dependency, "__name__", type(self.dependency).__name__)
50
+ cache = "" if self.use_cache else ", use_cache=False"
51
+ return f"{self.__class__.__name__}({attr}{cache})"
@@ -0,0 +1,17 @@
1
+ from typing import Dict
2
+
3
+ from fast_depends.types import AnyCallable
4
+
5
+
6
+ class Provider:
7
+ def __init__(self):
8
+ self.dependency_overrides: Dict[AnyCallable, AnyCallable] = {}
9
+
10
+ def override(self, original: AnyCallable, override: AnyCallable) -> None:
11
+ self.dependency_overrides[original] = override
12
+
13
+ def clear(self) -> None:
14
+ self.dependency_overrides = {}
15
+
16
+
17
+ dependency_provider = Provider()
@@ -0,0 +1,8 @@
1
+ from typing import Any, Callable, Dict, TypeVar
2
+ from typing_extensions import ParamSpec, TypeAlias
3
+
4
+ P = ParamSpec("P")
5
+
6
+ AnyCallable = TypeVar("AnyCallable", bound=Callable[..., Any])
7
+ DecoratedCallable: TypeAlias = AnyCallable
8
+ AnyDict = TypeVar("AnyDict", bound=Dict[str, Any])
@@ -0,0 +1,106 @@
1
+ from contextlib import AsyncExitStack, ExitStack
2
+ from functools import partial, wraps
3
+ from typing import Any, Callable, Optional, TypeVar
4
+
5
+ from pydantic import BaseModel, ValidationError, create_model
6
+
7
+ from fast_depends import model
8
+ from fast_depends.construct import get_dependant
9
+ from fast_depends.injector import (
10
+ is_coroutine_callable,
11
+ run_async,
12
+ solve_dependencies_async,
13
+ solve_dependencies_sync,
14
+ )
15
+ from fast_depends.provider import dependency_provider
16
+ from fast_depends.types import AnyCallable, P
17
+ from fast_depends.utils import args_to_kwargs
18
+
19
+ T = TypeVar("T")
20
+
21
+
22
+ def Depends( # noqa: N802
23
+ dependency: Optional[AnyCallable] = None, *, use_cache: bool = True
24
+ ) -> Any:
25
+ return model.Depends(dependency=dependency, use_cache=use_cache)
26
+
27
+
28
+ def inject(
29
+ func: Callable[P, T],
30
+ *,
31
+ dependency_overrides_provider: Optional[Any] = dependency_provider,
32
+ ) -> Callable[P, T]:
33
+ dependant = get_dependant(call=func, path=func.__name__)
34
+ error_model = create_model(func.__name__)
35
+
36
+ if is_coroutine_callable(func) is True:
37
+ f = async_typed_wrapper
38
+ else:
39
+ f = sync_typed_wrapper
40
+
41
+ return wraps(func)(
42
+ partial(
43
+ f,
44
+ dependant=dependant,
45
+ error_model=error_model,
46
+ dependency_overrides_provider=dependency_overrides_provider,
47
+ )
48
+ )
49
+
50
+
51
+ async def async_typed_wrapper(
52
+ *args: P.args,
53
+ dependant: model.Dependant,
54
+ error_model: BaseModel,
55
+ dependency_overrides_provider: Optional[Any],
56
+ **kwargs: P.kwargs,
57
+ ) -> Any:
58
+ kwargs = args_to_kwargs((x.name for x in dependant.params), *args, **kwargs)
59
+
60
+ async with AsyncExitStack() as stack:
61
+ solved_result, errors, _ = await solve_dependencies_async(
62
+ body=kwargs,
63
+ dependant=dependant,
64
+ stack=stack,
65
+ dependency_overrides_provider=dependency_overrides_provider,
66
+ )
67
+
68
+ if errors:
69
+ raise ValidationError(errors, error_model)
70
+
71
+ v, errors = dependant.cast_response(
72
+ await run_async(dependant=dependant, values=solved_result)
73
+ )
74
+
75
+ if errors:
76
+ raise ValidationError(errors, error_model)
77
+
78
+ return v
79
+
80
+
81
+ def sync_typed_wrapper(
82
+ *args: P.args,
83
+ dependant: model.Dependant,
84
+ error_model: BaseModel,
85
+ dependency_overrides_provider: Optional[Any],
86
+ **kwargs: P.kwargs,
87
+ ) -> Any:
88
+ kwargs = args_to_kwargs((x.name for x in dependant.params), *args, **kwargs)
89
+
90
+ with ExitStack() as stack:
91
+ solved_result, errors, _ = solve_dependencies_sync(
92
+ body=kwargs,
93
+ dependant=dependant,
94
+ stack=stack,
95
+ dependency_overrides_provider=dependency_overrides_provider,
96
+ )
97
+
98
+ if errors:
99
+ raise ValidationError(errors, error_model)
100
+
101
+ v, errors = dependant.cast_response(dependant.call(**solved_result))
102
+
103
+ if errors:
104
+ raise ValidationError(errors, error_model)
105
+
106
+ return v
@@ -0,0 +1,14 @@
1
+ from typing import Sequence
2
+
3
+ from fast_depends.types import AnyDict, P
4
+
5
+
6
+ def args_to_kwargs(
7
+ arguments: Sequence[str], *args: P.args, **kwargs: P.kwargs
8
+ ) -> AnyDict:
9
+ if not args:
10
+ return kwargs
11
+
12
+ unused = filter(lambda x: x not in kwargs, arguments)
13
+
14
+ return dict((*zip(unused, args), *kwargs.items()))
@@ -0,0 +1,190 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "fast-depends"
7
+ description = "FastDepends - extracted and cleared from HTTP domain Fastapi Dependency Injection System"
8
+ readme = "README.md"
9
+ authors = [
10
+ { name = "Pastukhov Nikita", email = "diementros@yandex.ru" },
11
+ ]
12
+
13
+ keywords = ["fastapi", "dependency injection"]
14
+
15
+ requires-python = ">=3.7"
16
+
17
+ classifiers = [
18
+ "Development Status :: 4 - Beta",
19
+ "License :: OSI Approved :: MIT License",
20
+ "Programming Language :: Python",
21
+ "Programming Language :: Python :: Implementation :: CPython",
22
+ "Programming Language :: Python :: 3",
23
+ "Programming Language :: Python :: 3 :: Only",
24
+ "Programming Language :: Python :: 3.7",
25
+ "Programming Language :: Python :: 3.8",
26
+ "Programming Language :: Python :: 3.9",
27
+ "Programming Language :: Python :: 3.10",
28
+ "Programming Language :: Python :: 3.11",
29
+ "Operating System :: OS Independent",
30
+ "Topic :: Software Development :: Libraries :: Python Modules",
31
+ "Topic :: Software Development :: Libraries",
32
+ "Topic :: Software Development",
33
+ "Typing :: Typed",
34
+ "Intended Audience :: Developers",
35
+ "Intended Audience :: Information Technology",
36
+ "Framework :: Pydantic",
37
+ "Framework :: Pydantic :: 1",
38
+ ]
39
+
40
+ dependencies = [
41
+ "pydantic>=1.8",
42
+ "anyio",
43
+ ]
44
+
45
+ dynamic = ["version"]
46
+
47
+ [project.urls]
48
+ Homepage = "https://lancetnik.github.io/FastDI/"
49
+ Documentation = "https://lancetnik.github.io/FastDI/"
50
+ Tracker = "https://github.com/Lancetnik/FastDI/issues"
51
+ Source = "https://github.com/Lancetnik/FastDI"
52
+
53
+ [project.optional-dependencies]
54
+ test = [
55
+ "coverage[toml]>=7.2",
56
+ "pytest>=7",
57
+ "pytest-asyncio>=0.21",
58
+ "pytest-xdist[psutil]",
59
+
60
+ "asyncmock; python_version < '3.8'",
61
+ ]
62
+
63
+ doc = [
64
+ "mkdocs-material >=8.1.4,<9.0.0",
65
+ "mkdocstrings[python]>=0.18",
66
+ "mdx-include >=1.4.1,<2.0.0",
67
+ "mkdocs-markdownextradata-plugin >=0.1.7,<0.3.0",
68
+ ]
69
+
70
+ dev = [
71
+ "FastDepends[test]",
72
+ "FastDepends[doc]",
73
+
74
+ "mypy>=1.1",
75
+ "black>=23.3.0",
76
+ "isort>=5",
77
+ "ruff>=0.0.260",
78
+ ]
79
+
80
+ [tool.hatch.metadata]
81
+ allow-direct-references = true
82
+ allow-ambiguous-features = true
83
+
84
+ [tool.hatch.version]
85
+ path = "fast_depends/__about__.py"
86
+
87
+ [tool.hatch.build]
88
+ skip-excluded-dirs = true
89
+ exclude = [
90
+ "/tests",
91
+ "/docs",
92
+ ]
93
+
94
+ [tool.hatch.envs.default]
95
+ python = "3.11"
96
+ skip-install = false
97
+ features = [
98
+ "dev",
99
+ ]
100
+
101
+ [tool.hatch.envs.test]
102
+ features = [
103
+ "test",
104
+ ]
105
+
106
+ [tool.hatch.envs.test.scripts]
107
+ run = "pytest -q"
108
+
109
+ [[tool.hatch.envs.test.matrix]]
110
+ python = ["37", "38", "39", "310", "311"]
111
+
112
+ [tool.hatch.envs.test-last]
113
+ python = "3.11"
114
+ features = [
115
+ "test",
116
+ ]
117
+
118
+ [tool.hatch.envs.test-last.scripts]
119
+ run = "pytest -q"
120
+ cov = "bash ./scripts/test-cov.sh -v"
121
+
122
+ [tool.mypy]
123
+ strict = true
124
+ ignore_missing_imports = true
125
+
126
+ [tool.isort]
127
+ profile = "black"
128
+ known_third_party = ["pydantic", "anyio"]
129
+
130
+ [tool.ruff]
131
+ select = [
132
+ "E", # pycodestyle errors
133
+ "W", # pycodestyle warnings
134
+ "F", # pyflakes
135
+ "I", # isort
136
+ "C", # flake8-comprehensions
137
+ "B", # flake8-bugbear
138
+ ]
139
+ ignore = [
140
+ "E501", # line too long, handled by black
141
+ "C901", # too complex
142
+ ]
143
+
144
+ [tool.pytest.ini_options]
145
+ minversion = "7.0"
146
+ addopts = "-q"
147
+ testpaths = [
148
+ "tests",
149
+ ]
150
+
151
+ [tool.coverage.run]
152
+ parallel = true
153
+ branch = true
154
+ concurrency = [
155
+ "multiprocessing",
156
+ "thread"
157
+ ]
158
+ source = [
159
+ "fastdi",
160
+ "tests"
161
+ ]
162
+ context = '${CONTEXT}'
163
+ omit = [
164
+ "**/__init__.py",
165
+ ]
166
+
167
+ [tool.coverage.report]
168
+ show_missing = true
169
+ skip_empty = true
170
+ exclude_lines = [
171
+ ".*# pragma: no cover",
172
+ "if __name__ == .__main__.:",
173
+ "self.logger",
174
+ "def __repr__",
175
+ "lambda: None",
176
+ "from .*",
177
+ "import .*",
178
+ '@(abc\.)?abstractmethod',
179
+ "raise NotImplementedError",
180
+ 'raise AssertionError',
181
+ 'raise ValueError',
182
+ 'logger\..*',
183
+ "pass",
184
+ '\.\.\.',
185
+ ]
186
+ omit = [
187
+ '*/__about__.py',
188
+ '*/__main__.py',
189
+ '*/__init__.py',
190
+ ]
@@ -0,0 +1,4 @@
1
+ mypy fastdi tests
2
+ ruff fastdi tests --fix
3
+ black fastdi tests
4
+ isort fastdi tests
@@ -0,0 +1,3 @@
1
+ hatch clean
2
+ hatch build
3
+ hatch publish
@@ -0,0 +1,5 @@
1
+ bash scripts/test.sh "$@"
2
+ coverage combine
3
+ coverage report --show-missing
4
+
5
+ rm .coverage*
@@ -0,0 +1 @@
1
+ coverage run -m pytest "$@"
@@ -0,0 +1,13 @@
1
+ import contextlib
2
+ from fast_depends import inject, Depends
3
+
4
+ @contextlib.contextmanager
5
+ def dep():
6
+ print("here")
7
+ return 1
8
+
9
+ @inject
10
+ def func(d = Depends(dep)):
11
+ return d
12
+
13
+ func()