anydi 0.41.0__py3-none-any.whl → 0.43.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 +65 -380
- anydi/_context.py +2 -2
- anydi/_decorators.py +122 -0
- anydi/_module.py +77 -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 +2 -42
- 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 +1 -1
- anydi/ext/starlette/middleware.py +1 -1
- anydi/testing.py +172 -0
- {anydi-0.41.0.dist-info → anydi-0.43.0.dist-info}/METADATA +2 -2
- anydi-0.43.0.dist-info/RECORD +34 -0
- anydi/_types.py +0 -145
- anydi-0.41.0.dist-info/RECORD +0 -28
- {anydi-0.41.0.dist-info → anydi-0.43.0.dist-info}/WHEEL +0 -0
- {anydi-0.41.0.dist-info → anydi-0.43.0.dist-info}/entry_points.txt +0 -0
- {anydi-0.41.0.dist-info → anydi-0.43.0.dist-info}/licenses/LICENSE +0 -0
anydi/_context.py
CHANGED
|
@@ -7,7 +7,7 @@ from typing import Any
|
|
|
7
7
|
|
|
8
8
|
from typing_extensions import Self
|
|
9
9
|
|
|
10
|
-
from .
|
|
10
|
+
from ._async import AsyncRLock, run_sync
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
class InstanceContext:
|
|
@@ -78,7 +78,7 @@ class InstanceContext:
|
|
|
78
78
|
exc_tb: TracebackType | None,
|
|
79
79
|
) -> bool:
|
|
80
80
|
"""Exit the context asynchronously."""
|
|
81
|
-
sync_exit = await
|
|
81
|
+
sync_exit = await run_sync(self.__exit__, exc_type, exc_val, exc_tb)
|
|
82
82
|
async_exit = await self._async_stack.__aexit__(exc_type, exc_val, exc_tb)
|
|
83
83
|
return bool(sync_exit) or bool(async_exit)
|
|
84
84
|
|
anydi/_decorators.py
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
from collections.abc import Iterable
|
|
2
|
+
from typing import (
|
|
3
|
+
TYPE_CHECKING,
|
|
4
|
+
Any,
|
|
5
|
+
Callable,
|
|
6
|
+
Concatenate,
|
|
7
|
+
ParamSpec,
|
|
8
|
+
Protocol,
|
|
9
|
+
TypedDict,
|
|
10
|
+
TypeGuard,
|
|
11
|
+
TypeVar,
|
|
12
|
+
overload,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
from ._module import Module
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
from ._scope import Scope
|
|
20
|
+
|
|
21
|
+
T = TypeVar("T")
|
|
22
|
+
P = ParamSpec("P")
|
|
23
|
+
|
|
24
|
+
ClassT = TypeVar("ClassT", bound=type)
|
|
25
|
+
ModuleT = TypeVar("ModuleT", bound="Module")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class ProvidedMetadata(TypedDict):
|
|
29
|
+
"""Metadata for classes marked as provided by AnyDI."""
|
|
30
|
+
|
|
31
|
+
scope: Scope
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def provided(*, scope: Scope) -> Callable[[ClassT], ClassT]:
|
|
35
|
+
"""Decorator for marking a class as provided by AnyDI with a specific scope."""
|
|
36
|
+
|
|
37
|
+
def decorator(cls: ClassT) -> ClassT:
|
|
38
|
+
cls.__provided__ = ProvidedMetadata(scope=scope)
|
|
39
|
+
return cls
|
|
40
|
+
|
|
41
|
+
return decorator
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
# Scoped decorators for class-level providers
|
|
45
|
+
transient = provided(scope="transient")
|
|
46
|
+
request = provided(scope="request")
|
|
47
|
+
singleton = provided(scope="singleton")
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class Provided(Protocol):
|
|
51
|
+
__provided__: ProvidedMetadata
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def is_provided(cls: Any) -> TypeGuard[type[Provided]]:
|
|
55
|
+
return hasattr(cls, "__provided__")
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class ProviderMetadata(TypedDict):
|
|
59
|
+
scope: Scope
|
|
60
|
+
override: bool
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def provider(
|
|
64
|
+
*, scope: Scope, override: bool = False
|
|
65
|
+
) -> Callable[
|
|
66
|
+
[Callable[Concatenate[ModuleT, P], T]], Callable[Concatenate[ModuleT, P], T]
|
|
67
|
+
]:
|
|
68
|
+
"""Decorator for marking a function or method as a provider in a AnyDI module."""
|
|
69
|
+
|
|
70
|
+
def decorator(
|
|
71
|
+
target: Callable[Concatenate[ModuleT, P], T],
|
|
72
|
+
) -> Callable[Concatenate[ModuleT, P], T]:
|
|
73
|
+
target.__provider__ = ProviderMetadata(scope=scope, override=override) # type: ignore
|
|
74
|
+
return target
|
|
75
|
+
|
|
76
|
+
return decorator
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class Provider(Protocol):
|
|
80
|
+
__provider__: ProviderMetadata
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def is_provider(obj: Callable[..., Any]) -> TypeGuard[Provider]:
|
|
84
|
+
return hasattr(obj, "__provider__")
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class InjectableMetadata(TypedDict):
|
|
88
|
+
tags: Iterable[str] | None
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@overload
|
|
92
|
+
def injectable(func: Callable[P, T]) -> Callable[P, T]: ...
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
@overload
|
|
96
|
+
def injectable(
|
|
97
|
+
*, tags: Iterable[str] | None = None
|
|
98
|
+
) -> Callable[[Callable[P, T]], Callable[P, T]]: ...
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def injectable(
|
|
102
|
+
func: Callable[P, T] | None = None,
|
|
103
|
+
tags: Iterable[str] | None = None,
|
|
104
|
+
) -> Callable[[Callable[P, T]], Callable[P, T]] | Callable[P, T]:
|
|
105
|
+
"""Decorator for marking a function or method as requiring dependency injection."""
|
|
106
|
+
|
|
107
|
+
def decorator(inner: Callable[P, T]) -> Callable[P, T]:
|
|
108
|
+
inner.__injectable__ = InjectableMetadata(tags=tags) # type: ignore
|
|
109
|
+
return inner
|
|
110
|
+
|
|
111
|
+
if func is None:
|
|
112
|
+
return decorator
|
|
113
|
+
|
|
114
|
+
return decorator(func)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
class Injectable(Protocol):
|
|
118
|
+
__injectable__: InjectableMetadata
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def is_injectable(obj: Callable[..., Any]) -> TypeGuard[Injectable]:
|
|
122
|
+
return hasattr(obj, "__injectable__")
|
anydi/_module.py
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import importlib
|
|
4
|
+
import inspect
|
|
5
|
+
from typing import TYPE_CHECKING, Any, Callable
|
|
6
|
+
|
|
7
|
+
from ._decorators import ProviderMetadata, is_provider
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from ._container import Container
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ModuleMeta(type):
|
|
14
|
+
"""A metaclass used for the Module base class."""
|
|
15
|
+
|
|
16
|
+
def __new__(cls, name: str, bases: tuple[type, ...], attrs: dict[str, Any]) -> Any:
|
|
17
|
+
attrs["providers"] = [
|
|
18
|
+
(name, value.__provider__)
|
|
19
|
+
for name, value in attrs.items()
|
|
20
|
+
if is_provider(value)
|
|
21
|
+
]
|
|
22
|
+
return super().__new__(cls, name, bases, attrs)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class Module(metaclass=ModuleMeta):
|
|
26
|
+
"""A base class for defining AnyDI modules."""
|
|
27
|
+
|
|
28
|
+
providers: list[tuple[str, ProviderMetadata]]
|
|
29
|
+
|
|
30
|
+
def configure(self, container: Container) -> None:
|
|
31
|
+
"""Configure the AnyDI container with providers and their dependencies."""
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
ModuleDef = Module | type[Module] | Callable[["Container"], None] | str
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class ModuleRegistrar:
|
|
38
|
+
def __init__(self, container: Container) -> None:
|
|
39
|
+
self._container = container
|
|
40
|
+
|
|
41
|
+
def register(self, module: ModuleDef) -> None:
|
|
42
|
+
"""Register a module as a callable, module type, or module instance."""
|
|
43
|
+
# Callable Module
|
|
44
|
+
if inspect.isfunction(module):
|
|
45
|
+
module(self._container)
|
|
46
|
+
return
|
|
47
|
+
|
|
48
|
+
# Module path
|
|
49
|
+
if isinstance(module, str):
|
|
50
|
+
module = self.import_module_from_string(module)
|
|
51
|
+
|
|
52
|
+
# Class based Module or Module type
|
|
53
|
+
if inspect.isclass(module) and issubclass(module, Module):
|
|
54
|
+
module = module()
|
|
55
|
+
|
|
56
|
+
if isinstance(module, Module):
|
|
57
|
+
module.configure(self._container)
|
|
58
|
+
for provider_name, metadata in module.providers:
|
|
59
|
+
obj = getattr(module, provider_name)
|
|
60
|
+
self._container.provider(**metadata)(obj)
|
|
61
|
+
else:
|
|
62
|
+
raise TypeError(
|
|
63
|
+
"The module must be a callable, a module type, or a module instance."
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
@staticmethod
|
|
67
|
+
def import_module_from_string(dotted_path: str) -> Any:
|
|
68
|
+
"""Import a module or attribute from a dotted path."""
|
|
69
|
+
try:
|
|
70
|
+
module_path, _, attribute_name = dotted_path.rpartition(".")
|
|
71
|
+
if module_path:
|
|
72
|
+
module = importlib.import_module(module_path)
|
|
73
|
+
return getattr(module, attribute_name)
|
|
74
|
+
else:
|
|
75
|
+
return importlib.import_module(attribute_name)
|
|
76
|
+
except (ImportError, AttributeError) as exc:
|
|
77
|
+
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, Callable, Union
|
|
10
|
+
|
|
11
|
+
from ._decorators import is_injectable
|
|
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
|
+
def _scan_module(
|
|
77
|
+
self, module: ModuleType, *, tags: Iterable[str]
|
|
78
|
+
) -> list[ScannedDependency]:
|
|
79
|
+
"""Scan a module for decorated members."""
|
|
80
|
+
dependencies: list[ScannedDependency] = []
|
|
81
|
+
|
|
82
|
+
for _, member in inspect.getmembers(module, predicate=callable):
|
|
83
|
+
if getattr(member, "__module__", None) != module.__name__:
|
|
84
|
+
continue
|
|
85
|
+
|
|
86
|
+
if self._should_include_member(member, tags=tags):
|
|
87
|
+
dependencies.append(ScannedDependency(member=member, module=module))
|
|
88
|
+
|
|
89
|
+
return dependencies
|
|
90
|
+
|
|
91
|
+
@staticmethod
|
|
92
|
+
def _should_include_member(
|
|
93
|
+
member: Callable[..., Any], *, tags: Iterable[str]
|
|
94
|
+
) -> bool:
|
|
95
|
+
"""Determine if a member should be included based on tags or marker defaults."""
|
|
96
|
+
|
|
97
|
+
if is_injectable(member):
|
|
98
|
+
member_tags = set(member.__injectable__["tags"] or [])
|
|
99
|
+
if tags:
|
|
100
|
+
return bool(set(tags) & member_tags)
|
|
101
|
+
return True # No tags passed → include all injectables
|
|
102
|
+
|
|
103
|
+
# If no tags are passed and not explicitly injectable,
|
|
104
|
+
# check for parameter markers
|
|
105
|
+
if not tags:
|
|
106
|
+
for param in get_typed_parameters(member):
|
|
107
|
+
if is_marker(param.default):
|
|
108
|
+
return True
|
|
109
|
+
|
|
110
|
+
return False
|
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
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
from collections.abc import Iterator
|
|
4
|
-
from functools import wraps
|
|
5
4
|
from typing import Annotated, Any
|
|
6
5
|
|
|
7
6
|
from django.conf import settings
|
|
@@ -9,7 +8,6 @@ from django.core.cache import BaseCache, caches
|
|
|
9
8
|
from django.db import connections
|
|
10
9
|
from django.db.backends.base.base import BaseDatabaseWrapper
|
|
11
10
|
from django.urls import URLPattern, URLResolver, get_resolver
|
|
12
|
-
from typing_extensions import get_origin
|
|
13
11
|
|
|
14
12
|
from anydi import Container
|
|
15
13
|
|
|
@@ -29,14 +27,11 @@ def register_settings(
|
|
|
29
27
|
continue
|
|
30
28
|
|
|
31
29
|
container.register(
|
|
32
|
-
Annotated[
|
|
30
|
+
Annotated[type(setting_value), f"{prefix}{setting_name}"],
|
|
33
31
|
_get_setting_value(setting_value),
|
|
34
32
|
scope="singleton",
|
|
35
33
|
)
|
|
36
34
|
|
|
37
|
-
# Patch AnyDI to resolve Any types for annotated settings
|
|
38
|
-
_patch_any_typed_annotated(container, prefix=prefix)
|
|
39
|
-
|
|
40
35
|
|
|
41
36
|
def register_components(container: Container) -> None:
|
|
42
37
|
"""Register Django components into the container."""
|
|
@@ -75,7 +70,7 @@ def inject_urlpatterns(container: Container, *, urlconf: str) -> None:
|
|
|
75
70
|
if pattern.lookup_str.startswith("ninja."):
|
|
76
71
|
continue # pragma: no cover
|
|
77
72
|
pattern.callback = container.inject(pattern.callback)
|
|
78
|
-
pattern.callback._injected = True # type: ignore
|
|
73
|
+
pattern.callback._injected = True # type: ignore
|
|
79
74
|
|
|
80
75
|
|
|
81
76
|
def iter_urlpatterns(
|
|
@@ -91,38 +86,3 @@ def iter_urlpatterns(
|
|
|
91
86
|
|
|
92
87
|
def _get_setting_value(value: Any) -> Any:
|
|
93
88
|
return lambda: value
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
def _any_typed_interface(interface: Any, prefix: str) -> Any:
|
|
97
|
-
origin = get_origin(interface)
|
|
98
|
-
if origin is not Annotated:
|
|
99
|
-
return interface # pragma: no cover
|
|
100
|
-
named = interface.__metadata__[-1]
|
|
101
|
-
|
|
102
|
-
if isinstance(named, str) and named.startswith(prefix):
|
|
103
|
-
_, setting_name = named.rsplit(prefix, maxsplit=1)
|
|
104
|
-
return Annotated[Any, f"{prefix}{setting_name}"]
|
|
105
|
-
return interface
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
def _patch_any_typed_annotated(container: Container, *, prefix: str) -> None:
|
|
109
|
-
def _patch_resolve(resolve: Any) -> Any:
|
|
110
|
-
@wraps(resolve)
|
|
111
|
-
def wrapper(interface: Any) -> Any:
|
|
112
|
-
return resolve(_any_typed_interface(interface, prefix))
|
|
113
|
-
|
|
114
|
-
return wrapper
|
|
115
|
-
|
|
116
|
-
def _patch_aresolve(resolve: Any) -> Any:
|
|
117
|
-
@wraps(resolve)
|
|
118
|
-
async def wrapper(interface: Any) -> Any:
|
|
119
|
-
return await resolve(_any_typed_interface(interface, prefix))
|
|
120
|
-
|
|
121
|
-
return wrapper
|
|
122
|
-
|
|
123
|
-
container.resolve = _patch_resolve( # type: ignore[method-assign]
|
|
124
|
-
container.resolve
|
|
125
|
-
)
|
|
126
|
-
container.aresolve = _patch_aresolve( # type: ignore[method-assign]
|
|
127
|
-
container.aresolve
|
|
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
|