anydi 0.40.0__py3-none-any.whl → 0.42.0__py3-none-any.whl
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/__init__.py +8 -13
- anydi/_async.py +50 -0
- anydi/_container.py +59 -371
- anydi/_context.py +2 -2
- anydi/_decorators.py +80 -0
- anydi/_module.py +76 -0
- anydi/_provider.py +81 -0
- anydi/_scan.py +110 -0
- anydi/_scope.py +9 -0
- anydi/{_utils.py → _typing.py} +49 -69
- anydi/ext/_utils.py +4 -4
- anydi/ext/django/_utils.py +3 -3
- anydi/ext/django/apps.py +0 -3
- anydi/ext/django/ninja/__init__.py +3 -3
- anydi/ext/django/ninja/_operation.py +1 -1
- anydi/ext/django/ninja/_signature.py +1 -1
- anydi/ext/fastapi.py +2 -2
- anydi/ext/faststream.py +2 -2
- anydi/ext/pytest_plugin.py +7 -3
- anydi/ext/starlette/middleware.py +1 -1
- anydi/testing.py +172 -0
- {anydi-0.40.0.dist-info → anydi-0.42.0.dist-info}/METADATA +2 -2
- anydi-0.42.0.dist-info/RECORD +34 -0
- anydi/_types.py +0 -145
- anydi-0.40.0.dist-info/RECORD +0 -28
- {anydi-0.40.0.dist-info → anydi-0.42.0.dist-info}/WHEEL +0 -0
- {anydi-0.40.0.dist-info → anydi-0.42.0.dist-info}/entry_points.txt +0 -0
- {anydi-0.40.0.dist-info → anydi-0.42.0.dist-info}/licenses/LICENSE +0 -0
anydi/_decorators.py
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
from collections.abc import Iterable
|
|
2
|
+
from typing import Callable, Concatenate, ParamSpec, TypedDict, TypeVar, overload
|
|
3
|
+
|
|
4
|
+
from ._module import Module
|
|
5
|
+
from ._scope import Scope
|
|
6
|
+
|
|
7
|
+
T = TypeVar("T")
|
|
8
|
+
P = ParamSpec("P")
|
|
9
|
+
|
|
10
|
+
ClassT = TypeVar("ClassT", bound=type)
|
|
11
|
+
ModuleT = TypeVar("ModuleT", bound=Module)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def provided(*, scope: Scope) -> Callable[[ClassT], ClassT]:
|
|
15
|
+
"""Decorator for marking a class as provided by AnyDI with a specific scope."""
|
|
16
|
+
|
|
17
|
+
def decorator(cls: ClassT) -> ClassT:
|
|
18
|
+
cls.__provided__ = True
|
|
19
|
+
cls.__scope__ = scope
|
|
20
|
+
return cls
|
|
21
|
+
|
|
22
|
+
return decorator
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
# Scoped decorators for class-level providers
|
|
26
|
+
transient = provided(scope="transient")
|
|
27
|
+
request = provided(scope="request")
|
|
28
|
+
singleton = provided(scope="singleton")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class ProviderMetadata(TypedDict):
|
|
32
|
+
scope: Scope
|
|
33
|
+
override: bool
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def provider(
|
|
37
|
+
*, scope: Scope, override: bool = False
|
|
38
|
+
) -> Callable[
|
|
39
|
+
[Callable[Concatenate[ModuleT, P], T]], Callable[Concatenate[ModuleT, P], T]
|
|
40
|
+
]:
|
|
41
|
+
"""Decorator for marking a function or method as a provider in a AnyDI module."""
|
|
42
|
+
|
|
43
|
+
def decorator(
|
|
44
|
+
target: Callable[Concatenate[ModuleT, P], T],
|
|
45
|
+
) -> Callable[Concatenate[ModuleT, P], T]:
|
|
46
|
+
target.__provider__ = ProviderMetadata(scope=scope, override=override) # type: ignore
|
|
47
|
+
return target
|
|
48
|
+
|
|
49
|
+
return decorator
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class InjectableMetadata(TypedDict):
|
|
53
|
+
wrapped: bool
|
|
54
|
+
tags: Iterable[str] | None
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@overload
|
|
58
|
+
def injectable(func: Callable[P, T]) -> Callable[P, T]: ...
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@overload
|
|
62
|
+
def injectable(
|
|
63
|
+
*, tags: Iterable[str] | None = None
|
|
64
|
+
) -> Callable[[Callable[P, T]], Callable[P, T]]: ...
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def injectable(
|
|
68
|
+
func: Callable[P, T] | None = None,
|
|
69
|
+
tags: Iterable[str] | None = None,
|
|
70
|
+
) -> Callable[[Callable[P, T]], Callable[P, T]] | Callable[P, T]:
|
|
71
|
+
"""Decorator for marking a function or method as requiring dependency injection."""
|
|
72
|
+
|
|
73
|
+
def decorator(inner: Callable[P, T]) -> Callable[P, T]:
|
|
74
|
+
inner.__injectable__ = InjectableMetadata(wrapped=True, tags=tags) # type: ignore
|
|
75
|
+
return inner
|
|
76
|
+
|
|
77
|
+
if func is None:
|
|
78
|
+
return decorator
|
|
79
|
+
|
|
80
|
+
return decorator(func)
|
anydi/_module.py
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import importlib
|
|
4
|
+
import inspect
|
|
5
|
+
from typing import TYPE_CHECKING, Any, Callable
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from ._container import Container
|
|
9
|
+
from ._decorators import ProviderMetadata
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ModuleMeta(type):
|
|
13
|
+
"""A metaclass used for the Module base class."""
|
|
14
|
+
|
|
15
|
+
def __new__(cls, name: str, bases: tuple[type, ...], attrs: dict[str, Any]) -> Any:
|
|
16
|
+
attrs["providers"] = [
|
|
17
|
+
(name, getattr(value, "__provider__"))
|
|
18
|
+
for name, value in attrs.items()
|
|
19
|
+
if hasattr(value, "__provider__")
|
|
20
|
+
]
|
|
21
|
+
return super().__new__(cls, name, bases, attrs)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class Module(metaclass=ModuleMeta):
|
|
25
|
+
"""A base class for defining AnyDI modules."""
|
|
26
|
+
|
|
27
|
+
providers: list[tuple[str, ProviderMetadata]]
|
|
28
|
+
|
|
29
|
+
def configure(self, container: Container) -> None:
|
|
30
|
+
"""Configure the AnyDI container with providers and their dependencies."""
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
ModuleDef = Module | type[Module] | Callable[["Container"], None] | str
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class ModuleRegistrar:
|
|
37
|
+
def __init__(self, container: Container) -> None:
|
|
38
|
+
self._container = container
|
|
39
|
+
|
|
40
|
+
def register(self, module: ModuleDef) -> None:
|
|
41
|
+
"""Register a module as a callable, module type, or module instance."""
|
|
42
|
+
# Callable Module
|
|
43
|
+
if inspect.isfunction(module):
|
|
44
|
+
module(self._container)
|
|
45
|
+
return
|
|
46
|
+
|
|
47
|
+
# Module path
|
|
48
|
+
if isinstance(module, str):
|
|
49
|
+
module = self.import_module_from_string(module)
|
|
50
|
+
|
|
51
|
+
# Class based Module or Module type
|
|
52
|
+
if inspect.isclass(module) and issubclass(module, Module):
|
|
53
|
+
module = module()
|
|
54
|
+
|
|
55
|
+
if isinstance(module, Module):
|
|
56
|
+
module.configure(self._container)
|
|
57
|
+
for provider_name, metadata in module.providers:
|
|
58
|
+
obj = getattr(module, provider_name)
|
|
59
|
+
self._container.provider(**metadata)(obj)
|
|
60
|
+
else:
|
|
61
|
+
raise TypeError(
|
|
62
|
+
"The module must be a callable, a module type, or a module instance."
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
@staticmethod
|
|
66
|
+
def import_module_from_string(dotted_path: str) -> Any:
|
|
67
|
+
"""Import a module or attribute from a dotted path."""
|
|
68
|
+
try:
|
|
69
|
+
module_path, _, attribute_name = dotted_path.rpartition(".")
|
|
70
|
+
if module_path:
|
|
71
|
+
module = importlib.import_module(module_path)
|
|
72
|
+
return getattr(module, attribute_name)
|
|
73
|
+
else:
|
|
74
|
+
return importlib.import_module(attribute_name)
|
|
75
|
+
except (ImportError, AttributeError) as exc:
|
|
76
|
+
raise ImportError(f"Cannot import '{dotted_path}': {exc}") from exc
|
anydi/_provider.py
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import enum
|
|
4
|
+
import inspect
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from functools import cached_property
|
|
7
|
+
from typing import Any, Callable, NamedTuple
|
|
8
|
+
|
|
9
|
+
from ._scope import Scope
|
|
10
|
+
from ._typing import NOT_SET
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ProviderKind(enum.IntEnum):
|
|
14
|
+
CLASS = 1
|
|
15
|
+
FUNCTION = 2
|
|
16
|
+
COROUTINE = 3
|
|
17
|
+
GENERATOR = 4
|
|
18
|
+
ASYNC_GENERATOR = 5
|
|
19
|
+
|
|
20
|
+
@classmethod
|
|
21
|
+
def from_call(cls, call: Callable[..., Any]) -> ProviderKind:
|
|
22
|
+
if inspect.isclass(call):
|
|
23
|
+
return cls.CLASS
|
|
24
|
+
if inspect.iscoroutinefunction(call):
|
|
25
|
+
return cls.COROUTINE
|
|
26
|
+
if inspect.isasyncgenfunction(call):
|
|
27
|
+
return cls.ASYNC_GENERATOR
|
|
28
|
+
if inspect.isgeneratorfunction(call):
|
|
29
|
+
return cls.GENERATOR
|
|
30
|
+
if inspect.isfunction(call) or inspect.ismethod(call):
|
|
31
|
+
return cls.FUNCTION
|
|
32
|
+
raise TypeError(
|
|
33
|
+
f"The provider `{call}` is invalid because it is not a callable object."
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
@classmethod
|
|
37
|
+
def is_resource(cls, kind: ProviderKind) -> bool:
|
|
38
|
+
return kind in (cls.GENERATOR, cls.ASYNC_GENERATOR)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass(kw_only=True, frozen=True)
|
|
42
|
+
class Provider:
|
|
43
|
+
call: Callable[..., Any]
|
|
44
|
+
scope: Scope
|
|
45
|
+
interface: Any
|
|
46
|
+
name: str
|
|
47
|
+
parameters: list[inspect.Parameter]
|
|
48
|
+
kind: ProviderKind
|
|
49
|
+
|
|
50
|
+
def __str__(self) -> str:
|
|
51
|
+
return self.name
|
|
52
|
+
|
|
53
|
+
@cached_property
|
|
54
|
+
def is_class(self) -> bool:
|
|
55
|
+
return self.kind == ProviderKind.CLASS
|
|
56
|
+
|
|
57
|
+
@cached_property
|
|
58
|
+
def is_coroutine(self) -> bool:
|
|
59
|
+
return self.kind == ProviderKind.COROUTINE
|
|
60
|
+
|
|
61
|
+
@cached_property
|
|
62
|
+
def is_generator(self) -> bool:
|
|
63
|
+
return self.kind == ProviderKind.GENERATOR
|
|
64
|
+
|
|
65
|
+
@cached_property
|
|
66
|
+
def is_async_generator(self) -> bool:
|
|
67
|
+
return self.kind == ProviderKind.ASYNC_GENERATOR
|
|
68
|
+
|
|
69
|
+
@cached_property
|
|
70
|
+
def is_async(self) -> bool:
|
|
71
|
+
return self.is_coroutine or self.is_async_generator
|
|
72
|
+
|
|
73
|
+
@cached_property
|
|
74
|
+
def is_resource(self) -> bool:
|
|
75
|
+
return ProviderKind.is_resource(self.kind)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class ProviderDef(NamedTuple):
|
|
79
|
+
call: Callable[..., Any]
|
|
80
|
+
scope: Scope
|
|
81
|
+
interface: Any = NOT_SET
|
anydi/_scan.py
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import importlib
|
|
4
|
+
import inspect
|
|
5
|
+
import pkgutil
|
|
6
|
+
from collections.abc import Iterable
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from types import ModuleType
|
|
9
|
+
from typing import TYPE_CHECKING, Any, Union
|
|
10
|
+
|
|
11
|
+
from ._decorators import InjectableMetadata
|
|
12
|
+
from ._typing import get_typed_parameters, is_marker
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from ._container import Container
|
|
16
|
+
|
|
17
|
+
Package = Union[ModuleType, str]
|
|
18
|
+
PackageOrIterable = Union[Package, Iterable[Package]]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass(kw_only=True)
|
|
22
|
+
class ScannedDependency:
|
|
23
|
+
member: Any
|
|
24
|
+
module: ModuleType
|
|
25
|
+
|
|
26
|
+
def __post_init__(self) -> None:
|
|
27
|
+
# Unwrap decorated functions if necessary
|
|
28
|
+
if hasattr(self.member, "__wrapped__"):
|
|
29
|
+
self.member = self.member.__wrapped__
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class Scanner:
|
|
33
|
+
def __init__(self, container: Container) -> None:
|
|
34
|
+
self._container = container
|
|
35
|
+
|
|
36
|
+
def scan(
|
|
37
|
+
self, /, packages: PackageOrIterable, *, tags: Iterable[str] | None = None
|
|
38
|
+
) -> None:
|
|
39
|
+
"""Scan packages or modules for decorated members and inject dependencies."""
|
|
40
|
+
if isinstance(packages, (ModuleType, str)):
|
|
41
|
+
scan_packages: Iterable[Package] = [packages]
|
|
42
|
+
else:
|
|
43
|
+
scan_packages = packages
|
|
44
|
+
|
|
45
|
+
dependencies = [
|
|
46
|
+
dependency
|
|
47
|
+
for package in scan_packages
|
|
48
|
+
for dependency in self._scan_package(package, tags=tags)
|
|
49
|
+
]
|
|
50
|
+
|
|
51
|
+
for dependency in dependencies:
|
|
52
|
+
decorated = self._container.inject()(dependency.member)
|
|
53
|
+
setattr(dependency.module, dependency.member.__name__, decorated)
|
|
54
|
+
|
|
55
|
+
def _scan_package(
|
|
56
|
+
self, package: Package, *, tags: Iterable[str] | None = None
|
|
57
|
+
) -> list[ScannedDependency]:
|
|
58
|
+
"""Scan a package or module for decorated members."""
|
|
59
|
+
tags = list(tags) if tags else []
|
|
60
|
+
|
|
61
|
+
if isinstance(package, str):
|
|
62
|
+
package = importlib.import_module(package)
|
|
63
|
+
|
|
64
|
+
if not hasattr(package, "__path__"):
|
|
65
|
+
return self._scan_module(package, tags=tags)
|
|
66
|
+
|
|
67
|
+
dependencies: list[ScannedDependency] = []
|
|
68
|
+
for module_info in pkgutil.walk_packages(
|
|
69
|
+
package.__path__, prefix=package.__name__ + "."
|
|
70
|
+
):
|
|
71
|
+
module = importlib.import_module(module_info.name)
|
|
72
|
+
dependencies.extend(self._scan_module(module, tags=tags))
|
|
73
|
+
|
|
74
|
+
return dependencies
|
|
75
|
+
|
|
76
|
+
@staticmethod
|
|
77
|
+
def _scan_module(
|
|
78
|
+
module: ModuleType, *, tags: Iterable[str]
|
|
79
|
+
) -> list[ScannedDependency]:
|
|
80
|
+
"""Scan a module for decorated members."""
|
|
81
|
+
dependencies: list[ScannedDependency] = []
|
|
82
|
+
|
|
83
|
+
for _, member in inspect.getmembers(module, predicate=callable):
|
|
84
|
+
if getattr(member, "__module__", None) != module.__name__:
|
|
85
|
+
continue
|
|
86
|
+
|
|
87
|
+
metadata: InjectableMetadata = getattr(
|
|
88
|
+
member,
|
|
89
|
+
"__injectable__",
|
|
90
|
+
InjectableMetadata(wrapped=False, tags=[]),
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
should_include = False
|
|
94
|
+
if metadata["wrapped"]:
|
|
95
|
+
should_include = True
|
|
96
|
+
elif tags and metadata["tags"]:
|
|
97
|
+
should_include = bool(set(metadata["tags"]) & set(tags))
|
|
98
|
+
elif tags and not metadata["tags"]:
|
|
99
|
+
continue # tags are provided but member has none
|
|
100
|
+
|
|
101
|
+
if not should_include:
|
|
102
|
+
for param in get_typed_parameters(member):
|
|
103
|
+
if is_marker(param.default):
|
|
104
|
+
should_include = True
|
|
105
|
+
break
|
|
106
|
+
|
|
107
|
+
if should_include:
|
|
108
|
+
dependencies.append(ScannedDependency(member=member, module=module))
|
|
109
|
+
|
|
110
|
+
return dependencies
|
anydi/_scope.py
ADDED
anydi/{_utils.py → _typing.py}
RENAMED
|
@@ -3,29 +3,22 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import builtins
|
|
6
|
-
import functools
|
|
7
|
-
import importlib
|
|
8
6
|
import inspect
|
|
9
7
|
import re
|
|
10
8
|
import sys
|
|
11
9
|
from collections.abc import AsyncIterator, Iterator
|
|
12
|
-
from
|
|
13
|
-
from typing import Any, Callable, ForwardRef, TypeVar
|
|
10
|
+
from typing import Any, Callable, ForwardRef
|
|
14
11
|
|
|
15
|
-
import
|
|
16
|
-
from typing_extensions import ParamSpec, Self, get_args, get_origin
|
|
12
|
+
from typing_extensions import Self, get_args, get_origin
|
|
17
13
|
|
|
18
14
|
try:
|
|
19
15
|
from types import NoneType
|
|
20
16
|
except ImportError:
|
|
21
|
-
NoneType = type(None)
|
|
17
|
+
NoneType = type(None)
|
|
22
18
|
|
|
23
|
-
T = TypeVar("T")
|
|
24
|
-
P = ParamSpec("P")
|
|
25
19
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
"""Get the fully qualified name of an object."""
|
|
20
|
+
def type_repr(obj: Any) -> str:
|
|
21
|
+
"""Get a string representation of a type or object."""
|
|
29
22
|
if isinstance(obj, str):
|
|
30
23
|
return obj
|
|
31
24
|
|
|
@@ -36,8 +29,8 @@ def get_full_qualname(obj: Any) -> str:
|
|
|
36
29
|
origin = get_origin(obj)
|
|
37
30
|
# If origin exists, handle generics recursively
|
|
38
31
|
if origin:
|
|
39
|
-
args = ", ".join(
|
|
40
|
-
return f"{
|
|
32
|
+
args = ", ".join(type_repr(arg) for arg in get_args(obj))
|
|
33
|
+
return f"{type_repr(origin)}[{args}]"
|
|
41
34
|
|
|
42
35
|
# Substitute standard library prefixes for clarity
|
|
43
36
|
full_qualname = f"{module}.{qualname}"
|
|
@@ -100,62 +93,49 @@ def get_typed_parameters(obj: Callable[..., Any]) -> list[inspect.Parameter]:
|
|
|
100
93
|
]
|
|
101
94
|
|
|
102
95
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
/,
|
|
106
|
-
*args: P.args,
|
|
107
|
-
**kwargs: P.kwargs,
|
|
108
|
-
) -> T:
|
|
109
|
-
"""Runs the given function asynchronously using the `anyio` library."""
|
|
110
|
-
return await anyio.to_thread.run_sync(functools.partial(func, *args, **kwargs))
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
def import_string(dotted_path: str) -> Any:
|
|
114
|
-
"""
|
|
115
|
-
Import a module or a specific attribute from a module using its dotted string path.
|
|
116
|
-
"""
|
|
117
|
-
try:
|
|
118
|
-
module_path, _, attribute_name = dotted_path.rpartition(".")
|
|
119
|
-
if module_path:
|
|
120
|
-
module = importlib.import_module(module_path)
|
|
121
|
-
return getattr(module, attribute_name)
|
|
122
|
-
else:
|
|
123
|
-
return importlib.import_module(attribute_name)
|
|
124
|
-
except (ImportError, AttributeError) as exc:
|
|
125
|
-
raise ImportError(f"Cannot import '{dotted_path}': {exc}") from exc
|
|
126
|
-
|
|
96
|
+
class _Marker:
|
|
97
|
+
"""A marker class for marking dependencies."""
|
|
127
98
|
|
|
128
|
-
|
|
129
|
-
def __init__(self) -> None:
|
|
130
|
-
self._lock = anyio.Lock()
|
|
131
|
-
self._owner: anyio.TaskInfo | None = None
|
|
132
|
-
self._count = 0
|
|
99
|
+
__slots__ = ()
|
|
133
100
|
|
|
134
|
-
|
|
135
|
-
current_task = anyio.get_current_task()
|
|
136
|
-
if self._owner == current_task:
|
|
137
|
-
self._count += 1
|
|
138
|
-
else:
|
|
139
|
-
await self._lock.acquire()
|
|
140
|
-
self._owner = current_task
|
|
141
|
-
self._count = 1
|
|
142
|
-
|
|
143
|
-
def release(self) -> None:
|
|
144
|
-
if self._owner != anyio.get_current_task():
|
|
145
|
-
raise RuntimeError("Lock can only be released by the owner")
|
|
146
|
-
self._count -= 1
|
|
147
|
-
if self._count == 0:
|
|
148
|
-
self._owner = None
|
|
149
|
-
self._lock.release()
|
|
150
|
-
|
|
151
|
-
async def __aenter__(self) -> Self:
|
|
152
|
-
await self.acquire()
|
|
101
|
+
def __call__(self) -> Self:
|
|
153
102
|
return self
|
|
154
103
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
104
|
+
|
|
105
|
+
def Marker() -> Any:
|
|
106
|
+
return _Marker()
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def is_marker(obj: Any) -> bool:
|
|
110
|
+
"""Checks if an object is a marker."""
|
|
111
|
+
return isinstance(obj, _Marker)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class Event:
|
|
115
|
+
"""Represents an event object."""
|
|
116
|
+
|
|
117
|
+
__slots__ = ()
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def is_event_type(obj: Any) -> bool:
|
|
121
|
+
"""Checks if an object is an event type."""
|
|
122
|
+
return inspect.isclass(obj) and issubclass(obj, Event)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
class _Sentinel:
|
|
126
|
+
__slots__ = ("_name",)
|
|
127
|
+
|
|
128
|
+
def __init__(self, name: str) -> None:
|
|
129
|
+
self._name = name
|
|
130
|
+
|
|
131
|
+
def __repr__(self) -> str:
|
|
132
|
+
return f"<{self._name}>"
|
|
133
|
+
|
|
134
|
+
def __eq__(self, other: object) -> bool:
|
|
135
|
+
return self is other
|
|
136
|
+
|
|
137
|
+
def __hash__(self) -> int:
|
|
138
|
+
return id(self)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
NOT_SET = _Sentinel("NOT_SET")
|
anydi/ext/_utils.py
CHANGED
|
@@ -8,8 +8,8 @@ from typing import Annotated, Any, Callable
|
|
|
8
8
|
|
|
9
9
|
from typing_extensions import get_args, get_origin
|
|
10
10
|
|
|
11
|
-
from anydi import Container
|
|
12
|
-
from anydi.
|
|
11
|
+
from anydi._container import Container
|
|
12
|
+
from anydi._typing import type_repr
|
|
13
13
|
|
|
14
14
|
logger = logging.getLogger(__name__)
|
|
15
15
|
|
|
@@ -71,9 +71,9 @@ def patch_call_parameter(
|
|
|
71
71
|
|
|
72
72
|
if not container.strict and not container.is_registered(parameter.annotation):
|
|
73
73
|
logger.debug(
|
|
74
|
-
f"Callable `{
|
|
74
|
+
f"Callable `{type_repr(call)}` injected parameter "
|
|
75
75
|
f"`{parameter.name}` with an annotation of "
|
|
76
|
-
f"`{
|
|
76
|
+
f"`{type_repr(parameter.annotation)}` "
|
|
77
77
|
"is not registered. It will be registered at runtime with the "
|
|
78
78
|
"first call because it is running in non-strict mode."
|
|
79
79
|
)
|
anydi/ext/django/_utils.py
CHANGED
|
@@ -75,7 +75,7 @@ def inject_urlpatterns(container: Container, *, urlconf: str) -> None:
|
|
|
75
75
|
if pattern.lookup_str.startswith("ninja."):
|
|
76
76
|
continue # pragma: no cover
|
|
77
77
|
pattern.callback = container.inject(pattern.callback)
|
|
78
|
-
pattern.callback._injected = True # type: ignore
|
|
78
|
+
pattern.callback._injected = True # type: ignore
|
|
79
79
|
|
|
80
80
|
|
|
81
81
|
def iter_urlpatterns(
|
|
@@ -120,9 +120,9 @@ def _patch_any_typed_annotated(container: Container, *, prefix: str) -> None:
|
|
|
120
120
|
|
|
121
121
|
return wrapper
|
|
122
122
|
|
|
123
|
-
container.resolve = _patch_resolve( # type: ignore
|
|
123
|
+
container.resolve = _patch_resolve( # type: ignore
|
|
124
124
|
container.resolve
|
|
125
125
|
)
|
|
126
|
-
container.aresolve = _patch_aresolve( # type: ignore
|
|
126
|
+
container.aresolve = _patch_aresolve( # type: ignore
|
|
127
127
|
container.aresolve
|
|
128
128
|
)
|
anydi/ext/django/apps.py
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
import logging
|
|
4
3
|
import types
|
|
5
4
|
from typing import Callable, cast
|
|
6
5
|
|
|
@@ -14,8 +13,6 @@ import anydi
|
|
|
14
13
|
from ._settings import get_settings
|
|
15
14
|
from ._utils import inject_urlpatterns, register_components, register_settings
|
|
16
15
|
|
|
17
|
-
logger = logging.getLogger(__name__)
|
|
18
|
-
|
|
19
16
|
|
|
20
17
|
class ContainerConfig(AppConfig):
|
|
21
18
|
name = "anydi.ext.django"
|
|
@@ -11,6 +11,6 @@ from ._signature import ViewSignature
|
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
def patch_ninja() -> None:
|
|
14
|
-
operation.ViewSignature = ViewSignature # type: ignore
|
|
15
|
-
operation.Operation = Operation # type: ignore
|
|
16
|
-
operation.AsyncOperation = AsyncOperation # type: ignore
|
|
14
|
+
operation.ViewSignature = ViewSignature # type: ignore
|
|
15
|
+
operation.Operation = Operation # type: ignore
|
|
16
|
+
operation.AsyncOperation = AsyncOperation # type: ignore
|
|
@@ -54,7 +54,7 @@ class AsyncOperation(BaseAsyncOperation):
|
|
|
54
54
|
super().__init__(*args, **kwargs)
|
|
55
55
|
self.dependencies = self.signature.dependencies
|
|
56
56
|
|
|
57
|
-
async def run(self, request: HttpRequest, **kw: Any) -> HttpResponseBase:
|
|
57
|
+
async def run(self, request: HttpRequest, **kw: Any) -> HttpResponseBase:
|
|
58
58
|
error = await self._run_checks(request)
|
|
59
59
|
if error:
|
|
60
60
|
return error
|
|
@@ -11,7 +11,7 @@ from ninja.signature.details import (
|
|
|
11
11
|
)
|
|
12
12
|
from ninja.signature.utils import get_path_param_names, get_typed_signature
|
|
13
13
|
|
|
14
|
-
from anydi.
|
|
14
|
+
from anydi._typing import is_marker # noqa
|
|
15
15
|
|
|
16
16
|
|
|
17
17
|
class ViewSignature(BaseViewSignature):
|
anydi/ext/fastapi.py
CHANGED
|
@@ -10,8 +10,8 @@ from fastapi.dependencies.models import Dependant
|
|
|
10
10
|
from fastapi.routing import APIRoute
|
|
11
11
|
from starlette.requests import Request
|
|
12
12
|
|
|
13
|
-
from anydi import Container
|
|
14
|
-
from anydi.
|
|
13
|
+
from anydi._container import Container
|
|
14
|
+
from anydi._typing import get_typed_parameters
|
|
15
15
|
|
|
16
16
|
from ._utils import HasInterface, patch_call_parameter
|
|
17
17
|
from .starlette.middleware import RequestScopedMiddleware
|
anydi/ext/faststream.py
CHANGED
|
@@ -9,7 +9,7 @@ from faststream import ContextRepo
|
|
|
9
9
|
from faststream.broker.core.usecase import BrokerUsecase
|
|
10
10
|
|
|
11
11
|
from anydi import Container
|
|
12
|
-
from anydi.
|
|
12
|
+
from anydi._typing import get_typed_parameters
|
|
13
13
|
|
|
14
14
|
from ._utils import HasInterface, patch_call_parameter
|
|
15
15
|
|
|
@@ -21,7 +21,7 @@ def install(broker: BrokerUsecase[Any, Any], container: Container) -> None:
|
|
|
21
21
|
it to the broker. It also patches the broker handlers to inject the required
|
|
22
22
|
dependencies using AnyDI.
|
|
23
23
|
"""
|
|
24
|
-
broker._container = container # type: ignore
|
|
24
|
+
broker._container = container # type: ignore
|
|
25
25
|
|
|
26
26
|
for handler in _get_broken_handlers(broker):
|
|
27
27
|
call = handler._original_call # noqa
|
anydi/ext/pytest_plugin.py
CHANGED
|
@@ -6,11 +6,10 @@ from collections.abc import Iterator
|
|
|
6
6
|
from typing import Any, Callable, cast
|
|
7
7
|
|
|
8
8
|
import pytest
|
|
9
|
-
from _pytest.python import async_warn_and_skip
|
|
10
9
|
from anyio.pytest_plugin import extract_backend_and_options, get_runner
|
|
11
10
|
|
|
12
11
|
from anydi import Container
|
|
13
|
-
from anydi.
|
|
12
|
+
from anydi._typing import get_typed_parameters
|
|
14
13
|
|
|
15
14
|
logger = logging.getLogger(__name__)
|
|
16
15
|
|
|
@@ -117,7 +116,12 @@ def _anydi_ainject(
|
|
|
117
116
|
|
|
118
117
|
# Skip if the anyio backend is not available
|
|
119
118
|
if "anyio_backend" not in request.fixturenames:
|
|
120
|
-
|
|
119
|
+
msg = (
|
|
120
|
+
"To run async test functions with `anyio`, "
|
|
121
|
+
"please configure the `anyio` pytest plugin.\n"
|
|
122
|
+
"See: https://anyio.readthedocs.io/en/stable/testing.html"
|
|
123
|
+
)
|
|
124
|
+
pytest.fail(msg, pytrace=False)
|
|
121
125
|
|
|
122
126
|
async def _awrapper() -> None:
|
|
123
127
|
# Setup the container
|