anydi 0.48.2__tar.gz → 0.52.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.
- {anydi-0.48.2 → anydi-0.52.0}/PKG-INFO +11 -5
- {anydi-0.48.2 → anydi-0.52.0}/README.md +8 -2
- {anydi-0.48.2 → anydi-0.52.0}/anydi/_async.py +2 -1
- {anydi-0.48.2 → anydi-0.52.0}/anydi/_container.py +7 -5
- {anydi-0.48.2 → anydi-0.52.0}/anydi/_decorators.py +1 -2
- {anydi-0.48.2 → anydi-0.52.0}/anydi/_module.py +2 -1
- {anydi-0.48.2 → anydi-0.52.0}/anydi/_provider.py +2 -1
- {anydi-0.48.2 → anydi-0.52.0}/anydi/_scan.py +4 -4
- {anydi-0.48.2 → anydi-0.52.0}/anydi/_typing.py +5 -43
- anydi-0.52.0/anydi/ext/django/__init__.py +8 -0
- {anydi-0.48.2 → anydi-0.52.0}/anydi/ext/pydantic_settings.py +2 -2
- {anydi-0.48.2 → anydi-0.52.0}/anydi/ext/pytest_plugin.py +2 -2
- {anydi-0.48.2 → anydi-0.52.0}/anydi/testing.py +1 -2
- {anydi-0.48.2 → anydi-0.52.0}/pyproject.toml +8 -50
- anydi-0.48.2/anydi/ext/django/__init__.py +0 -9
- anydi-0.48.2/anydi/ext/django/_container.py +0 -18
- anydi-0.48.2/anydi/ext/django/_settings.py +0 -37
- anydi-0.48.2/anydi/ext/django/_utils.py +0 -89
- anydi-0.48.2/anydi/ext/django/apps.py +0 -88
- anydi-0.48.2/anydi/ext/django/middleware.py +0 -28
- anydi-0.48.2/anydi/ext/django/ninja/__init__.py +0 -16
- anydi-0.48.2/anydi/ext/django/ninja/_operation.py +0 -75
- anydi-0.48.2/anydi/ext/django/ninja/_signature.py +0 -68
- {anydi-0.48.2 → anydi-0.52.0}/anydi/__init__.py +0 -0
- {anydi-0.48.2 → anydi-0.52.0}/anydi/_context.py +0 -0
- {anydi-0.48.2 → anydi-0.52.0}/anydi/_scope.py +0 -0
- {anydi-0.48.2 → anydi-0.52.0}/anydi/ext/__init__.py +0 -0
- {anydi-0.48.2 → anydi-0.52.0}/anydi/ext/fastapi.py +0 -0
- {anydi-0.48.2 → anydi-0.52.0}/anydi/ext/faststream.py +0 -0
- {anydi-0.48.2 → anydi-0.52.0}/anydi/ext/starlette/__init__.py +0 -0
- {anydi-0.48.2 → anydi-0.52.0}/anydi/ext/starlette/middleware.py +0 -0
- {anydi-0.48.2 → anydi-0.52.0}/anydi/py.typed +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: anydi
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.52.0
|
|
4
4
|
Summary: Dependency Injection library
|
|
5
5
|
Keywords: dependency injection,dependencies,di,async,asyncio,application
|
|
6
6
|
Author: Anton Ruhlov
|
|
@@ -20,16 +20,16 @@ Classifier: Environment :: Web Environment
|
|
|
20
20
|
Classifier: Intended Audience :: Developers
|
|
21
21
|
Classifier: License :: OSI Approved :: MIT License
|
|
22
22
|
Classifier: Programming Language :: Python :: 3
|
|
23
|
-
Classifier: Programming Language :: Python :: 3.9
|
|
24
23
|
Classifier: Programming Language :: Python :: 3.10
|
|
25
24
|
Classifier: Programming Language :: Python :: 3.11
|
|
26
25
|
Classifier: Programming Language :: Python :: 3.12
|
|
27
26
|
Classifier: Programming Language :: Python :: 3.13
|
|
27
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
28
28
|
Classifier: Programming Language :: Python :: 3 :: Only
|
|
29
29
|
Requires-Dist: typing-extensions>=4.15.0,<5
|
|
30
30
|
Requires-Dist: anyio>=3.7.1
|
|
31
31
|
Requires-Dist: wrapt>=1.17.0,<2
|
|
32
|
-
Requires-Python: >=3.
|
|
32
|
+
Requires-Python: >=3.10.0, <3.15
|
|
33
33
|
Project-URL: Repository, https://github.com/antonrh/anydi
|
|
34
34
|
Description-Content-Type: text/markdown
|
|
35
35
|
|
|
@@ -58,7 +58,7 @@ http://anydi.readthedocs.io/
|
|
|
58
58
|
|
|
59
59
|
---
|
|
60
60
|
|
|
61
|
-
`AnyDI` is a modern, lightweight Dependency Injection library suitable for any synchronous or asynchronous applications with Python 3.
|
|
61
|
+
`AnyDI` is a modern, lightweight Dependency Injection library suitable for any synchronous or asynchronous applications with Python 3.10+, based on type annotations ([PEP 484](https://peps.python.org/pep-0484/)).
|
|
62
62
|
|
|
63
63
|
The key features are:
|
|
64
64
|
|
|
@@ -140,6 +140,12 @@ anydi.ext.fastapi.install(app, container)
|
|
|
140
140
|
|
|
141
141
|
## Django Ninja Example
|
|
142
142
|
|
|
143
|
+
### Install
|
|
144
|
+
|
|
145
|
+
```sh
|
|
146
|
+
pip install 'anydi-django[ninja]'
|
|
147
|
+
```
|
|
148
|
+
|
|
143
149
|
*container.py*
|
|
144
150
|
|
|
145
151
|
```python
|
|
@@ -161,7 +167,7 @@ def get_container() -> Container:
|
|
|
161
167
|
```python
|
|
162
168
|
INSTALLED_APPS = [
|
|
163
169
|
...
|
|
164
|
-
"
|
|
170
|
+
"anydi_django",
|
|
165
171
|
]
|
|
166
172
|
|
|
167
173
|
ANYDI = {
|
|
@@ -23,7 +23,7 @@ http://anydi.readthedocs.io/
|
|
|
23
23
|
|
|
24
24
|
---
|
|
25
25
|
|
|
26
|
-
`AnyDI` is a modern, lightweight Dependency Injection library suitable for any synchronous or asynchronous applications with Python 3.
|
|
26
|
+
`AnyDI` is a modern, lightweight Dependency Injection library suitable for any synchronous or asynchronous applications with Python 3.10+, based on type annotations ([PEP 484](https://peps.python.org/pep-0484/)).
|
|
27
27
|
|
|
28
28
|
The key features are:
|
|
29
29
|
|
|
@@ -105,6 +105,12 @@ anydi.ext.fastapi.install(app, container)
|
|
|
105
105
|
|
|
106
106
|
## Django Ninja Example
|
|
107
107
|
|
|
108
|
+
### Install
|
|
109
|
+
|
|
110
|
+
```sh
|
|
111
|
+
pip install 'anydi-django[ninja]'
|
|
112
|
+
```
|
|
113
|
+
|
|
108
114
|
*container.py*
|
|
109
115
|
|
|
110
116
|
```python
|
|
@@ -126,7 +132,7 @@ def get_container() -> Container:
|
|
|
126
132
|
```python
|
|
127
133
|
INSTALLED_APPS = [
|
|
128
134
|
...
|
|
129
|
-
"
|
|
135
|
+
"anydi_django",
|
|
130
136
|
]
|
|
131
137
|
|
|
132
138
|
ANYDI = {
|
|
@@ -9,11 +9,11 @@ import logging
|
|
|
9
9
|
import types
|
|
10
10
|
import uuid
|
|
11
11
|
from collections import defaultdict
|
|
12
|
-
from collections.abc import AsyncIterator, Iterable, Iterator
|
|
12
|
+
from collections.abc import AsyncIterator, Callable, Iterable, Iterator
|
|
13
13
|
from contextvars import ContextVar
|
|
14
|
-
from typing import Annotated, Any,
|
|
14
|
+
from typing import Annotated, Any, TypeVar, cast, get_args, get_origin, overload
|
|
15
15
|
|
|
16
|
-
from typing_extensions import ParamSpec, Self,
|
|
16
|
+
from typing_extensions import ParamSpec, Self, type_repr
|
|
17
17
|
|
|
18
18
|
from ._async import run_sync
|
|
19
19
|
from ._context import InstanceContext
|
|
@@ -33,7 +33,6 @@ from ._typing import (
|
|
|
33
33
|
is_inject_marker,
|
|
34
34
|
is_iterator_type,
|
|
35
35
|
is_none_type,
|
|
36
|
-
type_repr,
|
|
37
36
|
)
|
|
38
37
|
|
|
39
38
|
T = TypeVar("T", bound=Any)
|
|
@@ -808,7 +807,10 @@ class Container:
|
|
|
808
807
|
marker = metadata[-1]
|
|
809
808
|
new_metadata = metadata[:-1]
|
|
810
809
|
if new_metadata:
|
|
811
|
-
|
|
810
|
+
if hasattr(Annotated, "__getitem__"):
|
|
811
|
+
new_annotation = Annotated.__getitem__((origin, *new_metadata)) # type: ignore
|
|
812
|
+
else:
|
|
813
|
+
new_annotation = Annotated.__class_getitem__((origin, *new_metadata)) # type: ignore
|
|
812
814
|
else:
|
|
813
815
|
new_annotation = origin
|
|
814
816
|
return parameter.replace(annotation=new_annotation, default=marker)
|
|
@@ -2,9 +2,10 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import enum
|
|
4
4
|
import inspect
|
|
5
|
+
from collections.abc import Callable
|
|
5
6
|
from dataclasses import dataclass
|
|
6
7
|
from functools import cached_property
|
|
7
|
-
from typing import Any,
|
|
8
|
+
from typing import Any, NamedTuple
|
|
8
9
|
|
|
9
10
|
from ._scope import Scope
|
|
10
11
|
from ._typing import NOT_SET
|
|
@@ -3,10 +3,10 @@ from __future__ import annotations
|
|
|
3
3
|
import importlib
|
|
4
4
|
import inspect
|
|
5
5
|
import pkgutil
|
|
6
|
-
from collections.abc import Iterable
|
|
6
|
+
from collections.abc import Callable, Iterable
|
|
7
7
|
from dataclasses import dataclass
|
|
8
8
|
from types import ModuleType
|
|
9
|
-
from typing import TYPE_CHECKING, Any
|
|
9
|
+
from typing import TYPE_CHECKING, Any
|
|
10
10
|
|
|
11
11
|
from ._decorators import is_injectable
|
|
12
12
|
from ._typing import get_typed_parameters, is_inject_marker
|
|
@@ -14,8 +14,8 @@ from ._typing import get_typed_parameters, is_inject_marker
|
|
|
14
14
|
if TYPE_CHECKING:
|
|
15
15
|
from ._container import Container
|
|
16
16
|
|
|
17
|
-
Package =
|
|
18
|
-
PackageOrIterable =
|
|
17
|
+
Package = ModuleType | str
|
|
18
|
+
PackageOrIterable = Package | Iterable[Package]
|
|
19
19
|
|
|
20
20
|
|
|
21
21
|
@dataclass(kw_only=True)
|
|
@@ -3,44 +3,9 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import inspect
|
|
6
|
-
import
|
|
7
|
-
import
|
|
8
|
-
from
|
|
9
|
-
from typing import Any, Callable, ForwardRef, TypeVar
|
|
10
|
-
|
|
11
|
-
from typing_extensions import get_args, get_origin
|
|
12
|
-
|
|
13
|
-
try:
|
|
14
|
-
from types import NoneType
|
|
15
|
-
except ImportError:
|
|
16
|
-
NoneType = type(None)
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
T = TypeVar("T")
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
def type_repr(obj: Any) -> str:
|
|
23
|
-
"""Get a string representation of a type or object."""
|
|
24
|
-
if isinstance(obj, str):
|
|
25
|
-
return obj
|
|
26
|
-
|
|
27
|
-
# Get module and qualname with defaults to handle non-types directly
|
|
28
|
-
module = getattr(obj, "__module__", type(obj).__module__)
|
|
29
|
-
qualname = getattr(obj, "__qualname__", type(obj).__qualname__)
|
|
30
|
-
|
|
31
|
-
origin = get_origin(obj)
|
|
32
|
-
# If origin exists, handle generics recursively
|
|
33
|
-
if origin:
|
|
34
|
-
args = ", ".join(type_repr(arg) for arg in get_args(obj))
|
|
35
|
-
return f"{type_repr(origin)}[{args}]"
|
|
36
|
-
|
|
37
|
-
# Substitute standard library prefixes for clarity
|
|
38
|
-
full_qualname = f"{module}.{qualname}"
|
|
39
|
-
return re.sub(
|
|
40
|
-
r"\b(builtins|typing|typing_extensions|collections\.abc|types)\.",
|
|
41
|
-
"",
|
|
42
|
-
full_qualname,
|
|
43
|
-
)
|
|
6
|
+
from collections.abc import AsyncIterator, Callable, Iterator
|
|
7
|
+
from types import NoneType
|
|
8
|
+
from typing import Any, ForwardRef
|
|
44
9
|
|
|
45
10
|
|
|
46
11
|
def is_context_manager(obj: Any) -> bool:
|
|
@@ -68,11 +33,8 @@ def get_typed_annotation(
|
|
|
68
33
|
) -> Any:
|
|
69
34
|
"""Get the typed annotation of a callable object."""
|
|
70
35
|
if isinstance(annotation, str):
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
else:
|
|
74
|
-
ref = ForwardRef(annotation)
|
|
75
|
-
annotation = ref._evaluate(globalns, globalns, recursive_guard=frozenset()) # noqa
|
|
36
|
+
ref = ForwardRef(annotation, module=module)
|
|
37
|
+
annotation = ref._evaluate(globalns, globalns, recursive_guard=frozenset()) # type: ignore[reportDeprecated]
|
|
76
38
|
return annotation
|
|
77
39
|
|
|
78
40
|
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
raise ImportError(
|
|
2
|
+
"The Django extension requires additional dependencies.\n\n"
|
|
3
|
+
"Install one of the following extras:\n"
|
|
4
|
+
" pip install 'anydi-django' # for Django\n"
|
|
5
|
+
" pip install 'anydi-django[ninja]' # for Django Ninja\n\n"
|
|
6
|
+
"Then, instead of importing from 'anydi.ext.django', import directly from:\n"
|
|
7
|
+
" import anydi_django\n"
|
|
8
|
+
)
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
from collections.abc import Iterable
|
|
4
|
-
from typing import Annotated, Any
|
|
3
|
+
from collections.abc import Callable, Iterable
|
|
4
|
+
from typing import Annotated, Any
|
|
5
5
|
|
|
6
6
|
from pydantic.fields import ComputedFieldInfo, FieldInfo # noqa
|
|
7
7
|
from pydantic_settings import BaseSettings
|
|
@@ -2,8 +2,8 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import inspect
|
|
4
4
|
import logging
|
|
5
|
-
from collections.abc import Iterator
|
|
6
|
-
from typing import Any,
|
|
5
|
+
from collections.abc import Callable, Iterator
|
|
6
|
+
from typing import Any, cast
|
|
7
7
|
|
|
8
8
|
import pytest
|
|
9
9
|
from anyio.pytest_plugin import extract_backend_and_options, get_runner
|
|
@@ -5,14 +5,13 @@ from collections.abc import Iterable, Iterator, Sequence
|
|
|
5
5
|
from typing import Any, TypeVar
|
|
6
6
|
|
|
7
7
|
import wrapt # type: ignore
|
|
8
|
-
from typing_extensions import Self
|
|
8
|
+
from typing_extensions import Self, type_repr
|
|
9
9
|
|
|
10
10
|
from ._container import Container
|
|
11
11
|
from ._context import InstanceContext
|
|
12
12
|
from ._module import ModuleDef
|
|
13
13
|
from ._provider import Provider, ProviderDef
|
|
14
14
|
from ._scope import Scope
|
|
15
|
-
from ._typing import type_repr
|
|
16
15
|
|
|
17
16
|
T = TypeVar("T")
|
|
18
17
|
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "anydi"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.52.0"
|
|
4
4
|
description = "Dependency Injection library"
|
|
5
5
|
authors = [{ name = "Anton Ruhlov", email = "antonruhlov@gmail.com" }]
|
|
6
|
-
requires-python = ">=3.
|
|
6
|
+
requires-python = ">=3.10.0, <3.15"
|
|
7
7
|
readme = "README.md"
|
|
8
8
|
license = "MIT"
|
|
9
9
|
keywords = [
|
|
@@ -29,11 +29,11 @@ classifiers = [
|
|
|
29
29
|
"Intended Audience :: Developers",
|
|
30
30
|
"License :: OSI Approved :: MIT License",
|
|
31
31
|
"Programming Language :: Python :: 3",
|
|
32
|
-
"Programming Language :: Python :: 3.9",
|
|
33
32
|
"Programming Language :: Python :: 3.10",
|
|
34
33
|
"Programming Language :: Python :: 3.11",
|
|
35
34
|
"Programming Language :: Python :: 3.12",
|
|
36
35
|
"Programming Language :: Python :: 3.13",
|
|
36
|
+
"Programming Language :: Python :: 3.14",
|
|
37
37
|
"Programming Language :: Python :: 3 :: Only",
|
|
38
38
|
]
|
|
39
39
|
dependencies = [
|
|
@@ -50,22 +50,17 @@ anydi = "anydi.ext.pytest_plugin"
|
|
|
50
50
|
|
|
51
51
|
[dependency-groups]
|
|
52
52
|
dev = [
|
|
53
|
-
"ruff>=0.
|
|
53
|
+
"ruff>=0.14.0",
|
|
54
|
+
"pyright>=1.1.406",
|
|
54
55
|
"pytest>=8.4.0,<9",
|
|
55
|
-
"pytest-cov>=
|
|
56
|
+
"pytest-cov>=7.0.0",
|
|
57
|
+
"pytest-mock>=3.14.1",
|
|
56
58
|
"starlette>=0.37.2",
|
|
57
59
|
"fastapi>=0.100.0",
|
|
58
60
|
"httpx>=0.26.0",
|
|
59
|
-
"django~=4.2",
|
|
60
|
-
"django-stubs>=5.1.1,<6",
|
|
61
|
-
"django-ninja>=1.1.0,<2",
|
|
62
|
-
"pytest-django>=4.8.0,<5",
|
|
63
61
|
"faststream>=0.5.10,<0.6",
|
|
64
62
|
"redis>=5.0.4,<6",
|
|
65
63
|
"pydantic-settings>=2.4.0,<3",
|
|
66
|
-
"bump-my-version>=1.1.4",
|
|
67
|
-
"pyright>=1.1.405",
|
|
68
|
-
"pytest-mock>=3.14.1",
|
|
69
64
|
]
|
|
70
65
|
docs = [
|
|
71
66
|
"mkdocs>=1.4.2,<2",
|
|
@@ -82,7 +77,7 @@ module-root = "."
|
|
|
82
77
|
|
|
83
78
|
[tool.ruff]
|
|
84
79
|
line-length = 88
|
|
85
|
-
target-version = "
|
|
80
|
+
target-version = "py310"
|
|
86
81
|
include = [
|
|
87
82
|
"anydi/**/*.py",
|
|
88
83
|
"tests/**/*.py",
|
|
@@ -117,7 +112,6 @@ addopts = [
|
|
|
117
112
|
]
|
|
118
113
|
xfail_strict = true
|
|
119
114
|
junit_family = "xunit2"
|
|
120
|
-
DJANGO_SETTINGS_MODULE = "tests.ext.django.settings"
|
|
121
115
|
|
|
122
116
|
[tool.coverage.report]
|
|
123
117
|
exclude_also = [
|
|
@@ -133,39 +127,3 @@ exclude_also = [
|
|
|
133
127
|
"if has_signature_eval_str_arg",
|
|
134
128
|
"if not anyio:",
|
|
135
129
|
]
|
|
136
|
-
|
|
137
|
-
[tool.coverage.run]
|
|
138
|
-
omit = [
|
|
139
|
-
"anydi/ext/django/ninja/_operation.py",
|
|
140
|
-
"anydi/ext/django/ninja/_signature.py",
|
|
141
|
-
]
|
|
142
|
-
|
|
143
|
-
[tool.bumpversion]
|
|
144
|
-
current_version = "0.48.2"
|
|
145
|
-
parse = """(?x)
|
|
146
|
-
(?P<major>0|[1-9]\\d*)\\.
|
|
147
|
-
(?P<minor>0|[1-9]\\d*)\\.
|
|
148
|
-
(?P<patch>0|[1-9]\\d*)
|
|
149
|
-
(?:
|
|
150
|
-
- # dash separator for pre-release section
|
|
151
|
-
(?P<pre_l>[a-zA-Z-]+) # pre-release label
|
|
152
|
-
(?P<pre_n>0|[1-9]\\d*) # pre-release version number
|
|
153
|
-
)? # pre-release section is optional
|
|
154
|
-
"""
|
|
155
|
-
serialize = [
|
|
156
|
-
"{major}.{minor}.{patch}-{pre_l}{pre_n}",
|
|
157
|
-
"{major}.{minor}.{patch}",
|
|
158
|
-
]
|
|
159
|
-
search = "{current_version}"
|
|
160
|
-
replace = "{new_version}"
|
|
161
|
-
message = "Bump version: {current_version} → {new_version}"
|
|
162
|
-
|
|
163
|
-
[[tool.bumpversion.files]]
|
|
164
|
-
filename = "pyproject.toml"
|
|
165
|
-
search = "version = \"{current_version}\""
|
|
166
|
-
replace = "version = \"{new_version}\""
|
|
167
|
-
|
|
168
|
-
[tool.bumpversion.parts.pre_l]
|
|
169
|
-
values = ["rc", "final"]
|
|
170
|
-
optional_value = "final"
|
|
171
|
-
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
from typing import cast
|
|
2
|
-
|
|
3
|
-
from django.apps.registry import apps
|
|
4
|
-
from django.utils.functional import SimpleLazyObject
|
|
5
|
-
|
|
6
|
-
import anydi
|
|
7
|
-
|
|
8
|
-
from .apps import ContainerConfig
|
|
9
|
-
|
|
10
|
-
__all__ = ["container"]
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
def _get_container() -> anydi.Container:
|
|
14
|
-
app_config = cast(ContainerConfig, apps.get_app_config(ContainerConfig.label))
|
|
15
|
-
return app_config.container
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
container = cast(anydi.Container, SimpleLazyObject(_get_container))
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
from collections.abc import Sequence
|
|
4
|
-
|
|
5
|
-
from django.conf import settings
|
|
6
|
-
from typing_extensions import TypedDict
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
class Settings(TypedDict):
|
|
10
|
-
CONTAINER_FACTORY: str | None
|
|
11
|
-
REGISTER_SETTINGS: bool
|
|
12
|
-
REGISTER_COMPONENTS: bool
|
|
13
|
-
INJECT_URLCONF: str | Sequence[str] | None
|
|
14
|
-
MODULES: Sequence[str]
|
|
15
|
-
SCAN_PACKAGES: Sequence[str]
|
|
16
|
-
PATCH_NINJA: bool
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
DEFAULTS = Settings(
|
|
20
|
-
CONTAINER_FACTORY=None,
|
|
21
|
-
REGISTER_SETTINGS=False,
|
|
22
|
-
REGISTER_COMPONENTS=False,
|
|
23
|
-
MODULES=[],
|
|
24
|
-
PATCH_NINJA=False,
|
|
25
|
-
INJECT_URLCONF=None,
|
|
26
|
-
SCAN_PACKAGES=[],
|
|
27
|
-
)
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
def get_settings() -> Settings:
|
|
31
|
-
"""Get the AnyDI settings from the Django settings."""
|
|
32
|
-
return Settings(
|
|
33
|
-
**{
|
|
34
|
-
**DEFAULTS,
|
|
35
|
-
**getattr(settings, "ANYDI", {}),
|
|
36
|
-
}
|
|
37
|
-
)
|
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
from collections.abc import Iterator
|
|
4
|
-
from typing import Annotated, Any
|
|
5
|
-
|
|
6
|
-
from django.conf import settings
|
|
7
|
-
from django.core.cache import BaseCache, caches
|
|
8
|
-
from django.db import connections
|
|
9
|
-
from django.db.backends.base.base import BaseDatabaseWrapper
|
|
10
|
-
from django.urls import URLPattern, URLResolver, get_resolver
|
|
11
|
-
|
|
12
|
-
from anydi import Container
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
def register_settings(
|
|
16
|
-
container: Container, prefix: str = "django.conf.settings."
|
|
17
|
-
) -> None:
|
|
18
|
-
"""Register Django settings into the container."""
|
|
19
|
-
|
|
20
|
-
# Ensure prefix ends with a dot
|
|
21
|
-
if prefix[-1] != ".":
|
|
22
|
-
prefix += "."
|
|
23
|
-
|
|
24
|
-
for setting_name in dir(settings):
|
|
25
|
-
setting_value = getattr(settings, setting_name)
|
|
26
|
-
if not setting_name.isupper():
|
|
27
|
-
continue
|
|
28
|
-
|
|
29
|
-
container.register(
|
|
30
|
-
Annotated[type(setting_value), f"{prefix}{setting_name}"],
|
|
31
|
-
_get_setting_value(setting_value),
|
|
32
|
-
scope="singleton",
|
|
33
|
-
)
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
def register_components(container: Container) -> None:
|
|
37
|
-
"""Register Django components into the container."""
|
|
38
|
-
|
|
39
|
-
# Register caches
|
|
40
|
-
def _get_cache(cache_name: str) -> Any:
|
|
41
|
-
return lambda: caches[cache_name]
|
|
42
|
-
|
|
43
|
-
for cache_name in caches:
|
|
44
|
-
container.register(
|
|
45
|
-
Annotated[BaseCache, cache_name],
|
|
46
|
-
_get_cache(cache_name),
|
|
47
|
-
scope="singleton",
|
|
48
|
-
)
|
|
49
|
-
|
|
50
|
-
# Register database connections
|
|
51
|
-
def _get_connection(alias: str) -> Any:
|
|
52
|
-
return lambda: connections[alias]
|
|
53
|
-
|
|
54
|
-
for alias in connections:
|
|
55
|
-
container.register(
|
|
56
|
-
Annotated[BaseDatabaseWrapper, alias],
|
|
57
|
-
_get_connection(alias),
|
|
58
|
-
scope="singleton",
|
|
59
|
-
)
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
def inject_urlpatterns(container: Container, *, urlconf: str) -> None:
|
|
63
|
-
"""Auto-inject the container into views."""
|
|
64
|
-
resolver = get_resolver(urlconf)
|
|
65
|
-
injected_urlpatterns = []
|
|
66
|
-
for pattern in iter_urlpatterns(resolver.url_patterns):
|
|
67
|
-
# Skip already injected views
|
|
68
|
-
if pattern.lookup_str in injected_urlpatterns:
|
|
69
|
-
continue
|
|
70
|
-
# Skip django-ninja views
|
|
71
|
-
if pattern.lookup_str.startswith("ninja."):
|
|
72
|
-
continue # pragma: no cover
|
|
73
|
-
pattern.callback = container.inject(pattern.callback)
|
|
74
|
-
injected_urlpatterns.append(pattern.lookup_str)
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
def iter_urlpatterns(
|
|
78
|
-
urlpatterns: list[URLPattern | URLResolver],
|
|
79
|
-
) -> Iterator[URLPattern]:
|
|
80
|
-
"""Iterate over all views in urlpatterns."""
|
|
81
|
-
for url_pattern in urlpatterns:
|
|
82
|
-
if isinstance(url_pattern, URLResolver):
|
|
83
|
-
yield from iter_urlpatterns(url_pattern.url_patterns)
|
|
84
|
-
else:
|
|
85
|
-
yield url_pattern
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
def _get_setting_value(value: Any) -> Any:
|
|
89
|
-
return lambda: value
|
|
@@ -1,88 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import types
|
|
4
|
-
from typing import Callable, cast
|
|
5
|
-
|
|
6
|
-
from django.apps import AppConfig
|
|
7
|
-
from django.conf import settings
|
|
8
|
-
from django.core.exceptions import ImproperlyConfigured
|
|
9
|
-
from django.utils.module_loading import import_string
|
|
10
|
-
|
|
11
|
-
import anydi
|
|
12
|
-
from anydi.testing import TestContainer
|
|
13
|
-
|
|
14
|
-
from ._settings import get_settings
|
|
15
|
-
from ._utils import inject_urlpatterns, register_components, register_settings
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
class ContainerConfig(AppConfig):
|
|
19
|
-
name = "anydi.ext.django"
|
|
20
|
-
label = "anydi_django"
|
|
21
|
-
|
|
22
|
-
def __init__(self, app_name: str, app_module: types.ModuleType | None) -> None:
|
|
23
|
-
super().__init__(app_name, app_module)
|
|
24
|
-
self.settings = get_settings()
|
|
25
|
-
# Create a container
|
|
26
|
-
container_factory_path = self.settings["CONTAINER_FACTORY"]
|
|
27
|
-
if container_factory_path:
|
|
28
|
-
try:
|
|
29
|
-
container_factory = cast(
|
|
30
|
-
Callable[[], anydi.Container], import_string(container_factory_path)
|
|
31
|
-
)
|
|
32
|
-
except ImportError as exc:
|
|
33
|
-
raise ImproperlyConfigured(
|
|
34
|
-
f"Cannot import container factory '{container_factory_path}'."
|
|
35
|
-
) from exc
|
|
36
|
-
container = container_factory()
|
|
37
|
-
else:
|
|
38
|
-
container = anydi.Container()
|
|
39
|
-
|
|
40
|
-
# Use test container
|
|
41
|
-
testing = getattr(settings, "ANYDI_TESTING", False)
|
|
42
|
-
if testing:
|
|
43
|
-
container = TestContainer.from_container(container)
|
|
44
|
-
|
|
45
|
-
self.container = container
|
|
46
|
-
|
|
47
|
-
def ready(self) -> None: # noqa: C901
|
|
48
|
-
# Register Django settings
|
|
49
|
-
if self.settings["REGISTER_SETTINGS"]:
|
|
50
|
-
register_settings(
|
|
51
|
-
self.container,
|
|
52
|
-
prefix=getattr(
|
|
53
|
-
settings,
|
|
54
|
-
"ANYDI_SETTINGS_PREFIX",
|
|
55
|
-
"django.conf.settings.",
|
|
56
|
-
),
|
|
57
|
-
)
|
|
58
|
-
|
|
59
|
-
# Register Django components
|
|
60
|
-
if self.settings["REGISTER_COMPONENTS"]:
|
|
61
|
-
register_components(self.container)
|
|
62
|
-
|
|
63
|
-
# Register modules
|
|
64
|
-
for module_path in self.settings["MODULES"]:
|
|
65
|
-
try:
|
|
66
|
-
module_cls = import_string(module_path)
|
|
67
|
-
except ImportError as exc:
|
|
68
|
-
raise ImproperlyConfigured(
|
|
69
|
-
f"Cannot import module '{module_path}'."
|
|
70
|
-
) from exc
|
|
71
|
-
self.container.register_module(module_cls)
|
|
72
|
-
|
|
73
|
-
# Patching the django-ninja framework if it installed
|
|
74
|
-
if self.settings["PATCH_NINJA"]:
|
|
75
|
-
from .ninja import patch_ninja
|
|
76
|
-
|
|
77
|
-
patch_ninja()
|
|
78
|
-
|
|
79
|
-
# Auto-injecting the container into views
|
|
80
|
-
if urlconf := self.settings["INJECT_URLCONF"]:
|
|
81
|
-
if isinstance(urlconf, str):
|
|
82
|
-
urlconf = [urlconf]
|
|
83
|
-
for u in urlconf:
|
|
84
|
-
inject_urlpatterns(self.container, urlconf=u)
|
|
85
|
-
|
|
86
|
-
# Scan packages
|
|
87
|
-
for scan_package in self.settings["SCAN_PACKAGES"]:
|
|
88
|
-
self.container.scan(scan_package)
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
from typing import Any, Callable
|
|
2
|
-
|
|
3
|
-
from asgiref.sync import iscoroutinefunction
|
|
4
|
-
from django.http import HttpRequest
|
|
5
|
-
from django.utils.decorators import sync_and_async_middleware
|
|
6
|
-
|
|
7
|
-
from ._container import container
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
@sync_and_async_middleware
|
|
11
|
-
def request_scoped_middleware(
|
|
12
|
-
get_response: Callable[..., Any],
|
|
13
|
-
) -> Callable[..., Any]:
|
|
14
|
-
if iscoroutinefunction(get_response):
|
|
15
|
-
|
|
16
|
-
async def async_middleware(request: HttpRequest) -> Any:
|
|
17
|
-
async with container.arequest_context() as context:
|
|
18
|
-
context.set(HttpRequest, request)
|
|
19
|
-
return await get_response(request)
|
|
20
|
-
|
|
21
|
-
return async_middleware
|
|
22
|
-
|
|
23
|
-
def middleware(request: HttpRequest) -> Any:
|
|
24
|
-
with container.request_context() as context:
|
|
25
|
-
context.set(HttpRequest, request)
|
|
26
|
-
return get_response(request)
|
|
27
|
-
|
|
28
|
-
return middleware
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
try:
|
|
2
|
-
from ninja import operation
|
|
3
|
-
except ImportError as exc: # pragma: no cover
|
|
4
|
-
raise ImportError(
|
|
5
|
-
"'django-ninja' is not installed. "
|
|
6
|
-
"Please install it using 'pip install django-ninja'."
|
|
7
|
-
) from exc
|
|
8
|
-
|
|
9
|
-
from ._operation import AsyncOperation, Operation
|
|
10
|
-
from ._signature import ViewSignature
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
def patch_ninja() -> None:
|
|
14
|
-
operation.ViewSignature = ViewSignature # type: ignore
|
|
15
|
-
operation.Operation = Operation # type: ignore
|
|
16
|
-
operation.AsyncOperation = AsyncOperation # type: ignore
|
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
from typing import Any
|
|
4
|
-
|
|
5
|
-
from django.http import HttpRequest, HttpResponseBase
|
|
6
|
-
from ninja.operation import (
|
|
7
|
-
AsyncOperation as BaseAsyncOperation, # noqa
|
|
8
|
-
Operation as BaseOperation,
|
|
9
|
-
)
|
|
10
|
-
|
|
11
|
-
from anydi.ext.django import container
|
|
12
|
-
|
|
13
|
-
from ._signature import ViewSignature
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
def _update_exc_args(exc: Exception) -> None:
|
|
17
|
-
if isinstance(exc, TypeError) and "required positional argument" in str(exc):
|
|
18
|
-
msg = "Did you fail to use functools.wraps() in a decorator?"
|
|
19
|
-
msg = f"{exc.args[0]}: {msg}" if exc.args else msg
|
|
20
|
-
exc.args = (msg,) + exc.args[1:]
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
class Operation(BaseOperation):
|
|
24
|
-
signature: ViewSignature
|
|
25
|
-
|
|
26
|
-
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
|
27
|
-
super().__init__(*args, **kwargs)
|
|
28
|
-
self.dependencies = self.signature.dependencies
|
|
29
|
-
|
|
30
|
-
def run(self, request: HttpRequest, **kw: Any) -> HttpResponseBase:
|
|
31
|
-
error = self._run_checks(request)
|
|
32
|
-
if error:
|
|
33
|
-
return error
|
|
34
|
-
try:
|
|
35
|
-
temporal_response = self.api.create_temporal_response(request)
|
|
36
|
-
values = self._get_values(request, kw, temporal_response)
|
|
37
|
-
values.update(self._get_dependencies())
|
|
38
|
-
result = self.view_func(request, **values)
|
|
39
|
-
return self._result_to_response(request, result, temporal_response)
|
|
40
|
-
except Exception as e:
|
|
41
|
-
_update_exc_args(e)
|
|
42
|
-
return self.api.on_exception(request, e)
|
|
43
|
-
|
|
44
|
-
def _get_dependencies(self) -> dict[str, Any]:
|
|
45
|
-
return {
|
|
46
|
-
name: container.resolve(interface) for name, interface in self.dependencies
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
class AsyncOperation(BaseAsyncOperation):
|
|
51
|
-
signature: ViewSignature
|
|
52
|
-
|
|
53
|
-
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
|
54
|
-
super().__init__(*args, **kwargs)
|
|
55
|
-
self.dependencies = self.signature.dependencies
|
|
56
|
-
|
|
57
|
-
async def run(self, request: HttpRequest, **kw: Any) -> HttpResponseBase:
|
|
58
|
-
error = await self._run_checks(request)
|
|
59
|
-
if error:
|
|
60
|
-
return error
|
|
61
|
-
try:
|
|
62
|
-
temporal_response = self.api.create_temporal_response(request)
|
|
63
|
-
values = self._get_values(request, kw, temporal_response)
|
|
64
|
-
values.update(await self._get_dependencies())
|
|
65
|
-
result = await self.view_func(request, **values)
|
|
66
|
-
return self._result_to_response(request, result, temporal_response)
|
|
67
|
-
except Exception as e:
|
|
68
|
-
_update_exc_args(e)
|
|
69
|
-
return self.api.on_exception(request, e)
|
|
70
|
-
|
|
71
|
-
async def _get_dependencies(self) -> dict[str, Any]:
|
|
72
|
-
return {
|
|
73
|
-
name: await container.aresolve(interface)
|
|
74
|
-
for name, interface in self.dependencies
|
|
75
|
-
}
|
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import inspect
|
|
4
|
-
from collections.abc import Callable
|
|
5
|
-
from typing import Any
|
|
6
|
-
|
|
7
|
-
from django.http import HttpResponse
|
|
8
|
-
from ninja.signature.details import (
|
|
9
|
-
FuncParam, # noqa
|
|
10
|
-
ViewSignature as BaseViewSignature,
|
|
11
|
-
)
|
|
12
|
-
from ninja.signature.utils import get_path_param_names, get_typed_signature
|
|
13
|
-
|
|
14
|
-
from anydi._typing import is_inject_marker # noqa
|
|
15
|
-
from anydi.ext.django import container
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
class ViewSignature(BaseViewSignature):
|
|
19
|
-
def __init__(self, path: str, view_func: Callable[..., Any]) -> None:
|
|
20
|
-
self.view_func = view_func
|
|
21
|
-
self.signature = get_typed_signature(self.view_func)
|
|
22
|
-
self.path = path
|
|
23
|
-
self.path_params_names = get_path_param_names(path)
|
|
24
|
-
self.docstring = inspect.cleandoc(view_func.__doc__ or "")
|
|
25
|
-
self.has_kwargs = False
|
|
26
|
-
self.dependencies = []
|
|
27
|
-
|
|
28
|
-
self.params = []
|
|
29
|
-
for name, arg in self.signature.parameters.items():
|
|
30
|
-
if name == "request":
|
|
31
|
-
# TODO: maybe better assert that 1st param is request or check by type?
|
|
32
|
-
# maybe even have attribute like `has_request`
|
|
33
|
-
# so that users can ignore passing request if not needed
|
|
34
|
-
continue
|
|
35
|
-
|
|
36
|
-
if arg.kind == arg.VAR_KEYWORD:
|
|
37
|
-
# Skipping **kwargs
|
|
38
|
-
self.has_kwargs = True
|
|
39
|
-
continue
|
|
40
|
-
|
|
41
|
-
if arg.kind == arg.VAR_POSITIONAL:
|
|
42
|
-
# Skipping *args
|
|
43
|
-
continue
|
|
44
|
-
|
|
45
|
-
if arg.annotation is HttpResponse:
|
|
46
|
-
self.response_arg = name
|
|
47
|
-
continue
|
|
48
|
-
|
|
49
|
-
interface, should_inject = container.validate_injected_parameter(
|
|
50
|
-
arg, call=self.view_func
|
|
51
|
-
)
|
|
52
|
-
if should_inject:
|
|
53
|
-
self.dependencies.append((name, interface))
|
|
54
|
-
continue
|
|
55
|
-
|
|
56
|
-
func_param = self._get_param_type(name, arg)
|
|
57
|
-
self.params.append(func_param)
|
|
58
|
-
|
|
59
|
-
ninja_contribute_args = getattr(view_func, "_ninja_contribute_args", None)
|
|
60
|
-
if ninja_contribute_args is not None:
|
|
61
|
-
for p_name, p_type, p_source in ninja_contribute_args:
|
|
62
|
-
self.params.append(
|
|
63
|
-
FuncParam(p_name, p_source.alias or p_name, p_source, p_type, False)
|
|
64
|
-
)
|
|
65
|
-
|
|
66
|
-
self.models = self._create_models()
|
|
67
|
-
|
|
68
|
-
self._validate_view_path_params()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|