anydi 0.42.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/_container.py CHANGED
@@ -11,12 +11,13 @@ import uuid
11
11
  from collections import defaultdict
12
12
  from collections.abc import AsyncIterator, Iterable, Iterator
13
13
  from contextvars import ContextVar
14
- from typing import Annotated, Any, Callable, TypeVar, cast, overload
14
+ from typing import Any, Callable, TypeVar, cast, overload
15
15
 
16
16
  from typing_extensions import ParamSpec, Self, get_args, get_origin
17
17
 
18
18
  from ._async import run_sync
19
19
  from ._context import InstanceContext
20
+ from ._decorators import is_provided
20
21
  from ._module import ModuleDef, ModuleRegistrar
21
22
  from ._provider import (
22
23
  Provider,
@@ -441,17 +442,13 @@ class Container:
441
442
  except LookupError:
442
443
  if self.strict or interface is inspect.Parameter.empty:
443
444
  raise
444
- if get_origin(interface) is Annotated and (args := get_args(interface)):
445
- call = args[0]
446
- else:
447
- call = interface
448
- if inspect.isclass(call) and not is_builtin_type(call):
445
+ if inspect.isclass(interface) and not is_builtin_type(interface):
449
446
  # Try to get defined scope
450
- if hasattr(call, "__scope__"):
451
- scope = getattr(call, "__scope__")
447
+ if is_provided(interface):
448
+ scope = interface.__provided__["scope"]
452
449
  else:
453
450
  scope = parent_scope
454
- return self._register_provider(call, scope, interface, **defaults)
451
+ return self._register_provider(interface, scope, interface, **defaults)
455
452
  raise
456
453
 
457
454
  def _set_provider(self, provider: Provider) -> None:
@@ -793,8 +790,8 @@ class Container:
793
790
  return cast(Callable[P, T], self._inject_cache[call])
794
791
 
795
792
  injected_params = self._get_injected_params(call)
796
-
797
793
  if not injected_params:
794
+ self._inject_cache[call] = call
798
795
  return call
799
796
 
800
797
  if inspect.iscoroutinefunction(call):
anydi/_decorators.py CHANGED
@@ -1,22 +1,41 @@
1
1
  from collections.abc import Iterable
2
- from typing import Callable, Concatenate, ParamSpec, TypedDict, TypeVar, overload
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
+
3
18
 
4
- from ._module import Module
5
19
  from ._scope import Scope
6
20
 
7
21
  T = TypeVar("T")
8
22
  P = ParamSpec("P")
9
23
 
10
24
  ClassT = TypeVar("ClassT", bound=type)
11
- ModuleT = TypeVar("ModuleT", bound=Module)
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
12
32
 
13
33
 
14
34
  def provided(*, scope: Scope) -> Callable[[ClassT], ClassT]:
15
35
  """Decorator for marking a class as provided by AnyDI with a specific scope."""
16
36
 
17
37
  def decorator(cls: ClassT) -> ClassT:
18
- cls.__provided__ = True
19
- cls.__scope__ = scope
38
+ cls.__provided__ = ProvidedMetadata(scope=scope)
20
39
  return cls
21
40
 
22
41
  return decorator
@@ -28,6 +47,14 @@ request = provided(scope="request")
28
47
  singleton = provided(scope="singleton")
29
48
 
30
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
+
31
58
  class ProviderMetadata(TypedDict):
32
59
  scope: Scope
33
60
  override: bool
@@ -49,8 +76,15 @@ def provider(
49
76
  return decorator
50
77
 
51
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
+
52
87
  class InjectableMetadata(TypedDict):
53
- wrapped: bool
54
88
  tags: Iterable[str] | None
55
89
 
56
90
 
@@ -71,10 +105,18 @@ def injectable(
71
105
  """Decorator for marking a function or method as requiring dependency injection."""
72
106
 
73
107
  def decorator(inner: Callable[P, T]) -> Callable[P, T]:
74
- inner.__injectable__ = InjectableMetadata(wrapped=True, tags=tags) # type: ignore
108
+ inner.__injectable__ = InjectableMetadata(tags=tags) # type: ignore
75
109
  return inner
76
110
 
77
111
  if func is None:
78
112
  return decorator
79
113
 
80
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 CHANGED
@@ -4,9 +4,10 @@ import importlib
4
4
  import inspect
5
5
  from typing import TYPE_CHECKING, Any, Callable
6
6
 
7
+ from ._decorators import ProviderMetadata, is_provider
8
+
7
9
  if TYPE_CHECKING:
8
10
  from ._container import Container
9
- from ._decorators import ProviderMetadata
10
11
 
11
12
 
12
13
  class ModuleMeta(type):
@@ -14,9 +15,9 @@ class ModuleMeta(type):
14
15
 
15
16
  def __new__(cls, name: str, bases: tuple[type, ...], attrs: dict[str, Any]) -> Any:
16
17
  attrs["providers"] = [
17
- (name, getattr(value, "__provider__"))
18
+ (name, value.__provider__)
18
19
  for name, value in attrs.items()
19
- if hasattr(value, "__provider__")
20
+ if is_provider(value)
20
21
  ]
21
22
  return super().__new__(cls, name, bases, attrs)
22
23
 
anydi/_scan.py CHANGED
@@ -6,9 +6,9 @@ import pkgutil
6
6
  from collections.abc import Iterable
7
7
  from dataclasses import dataclass
8
8
  from types import ModuleType
9
- from typing import TYPE_CHECKING, Any, Union
9
+ from typing import TYPE_CHECKING, Any, Callable, Union
10
10
 
11
- from ._decorators import InjectableMetadata
11
+ from ._decorators import is_injectable
12
12
  from ._typing import get_typed_parameters, is_marker
13
13
 
14
14
  if TYPE_CHECKING:
@@ -73,9 +73,8 @@ class Scanner:
73
73
 
74
74
  return dependencies
75
75
 
76
- @staticmethod
77
76
  def _scan_module(
78
- module: ModuleType, *, tags: Iterable[str]
77
+ self, module: ModuleType, *, tags: Iterable[str]
79
78
  ) -> list[ScannedDependency]:
80
79
  """Scan a module for decorated members."""
81
80
  dependencies: list[ScannedDependency] = []
@@ -84,27 +83,28 @@ class Scanner:
84
83
  if getattr(member, "__module__", None) != module.__name__:
85
84
  continue
86
85
 
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:
86
+ if self._should_include_member(member, tags=tags):
108
87
  dependencies.append(ScannedDependency(member=member, module=module))
109
88
 
110
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
@@ -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[Any, f"{prefix}{setting_name}"],
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."""
@@ -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
124
- container.resolve
125
- )
126
- container.aresolve = _patch_aresolve( # type: ignore
127
- container.aresolve
128
- )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: anydi
3
- Version: 0.42.0
3
+ Version: 0.43.0
4
4
  Summary: Dependency Injection library
5
5
  Project-URL: Repository, https://github.com/antonrh/anydi
6
6
  Author-email: Anton Ruhlov <antonruhlov@gmail.com>
@@ -1,11 +1,11 @@
1
1
  anydi/__init__.py,sha256=UH3N0LIw4ysoV0_EFgtDNioxEQxOmeytYbJnS8MXIXM,532
2
2
  anydi/_async.py,sha256=KhRd3RmZFcwNDzrMm8ctA1gwrg-6m_7laECTYsZdF5k,1445
3
- anydi/_container.py,sha256=Ap8PnH6ym7GTjBv9r2cmJkJCEY5XpzMXIkJYXXwJkrY,31611
3
+ anydi/_container.py,sha256=sftWiHUTJBMQelglSbThn-Y0IeYw15vZnmC7sFqDpxQ,31529
4
4
  anydi/_context.py,sha256=_Xy8cTpRskb4cxTd-Fe-5NnIZyBe1DnovkofhdeUfmw,3020
5
- anydi/_decorators.py,sha256=b5FPT86pnJX8moKGaOgMclZYNXIkLhWXB_7jg1nogX0,2110
6
- anydi/_module.py,sha256=DK--YVzvK1KpCbqJnfdB99_vEcjsqS6Xhqk7NNClqGs,2596
5
+ anydi/_decorators.py,sha256=F3yBeGQSz1EsulZaEvYn3cd6FEjJRMoyA6u1QCbEwcs,2813
6
+ anydi/_module.py,sha256=QPvP27JndZkwl-FYUZWscJm6yfkNzjwoFGURyDhb6Pc,2582
7
7
  anydi/_provider.py,sha256=ig2ecn-STmFGcpkLE5A5OM35XHtU2NsxFVrGp2CvuvM,2123
8
- anydi/_scan.py,sha256=ADA_gjsVLP7q43qU03bsTchEJwbpckknmDCLLMf7wss,3623
8
+ anydi/_scan.py,sha256=WOdsPaoRZPYw4kBzoBXyWhkiwuxY8_UjHfa0c9QFyLc,3639
9
9
  anydi/_scope.py,sha256=PFHjPb2-n0vhRo9mvD_craTFfoJBzR3y-N3_0apL5Q0,258
10
10
  anydi/_typing.py,sha256=m7KnVenDE1E420IeYz2Ocw6dhddmSFbuBBgkOXtl9pA,3709
11
11
  anydi/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -19,7 +19,7 @@ anydi/ext/pytest_plugin.py,sha256=57Y1G4MOpwV43Fib88JGeRI8jACcj5Jy8R-7ZT0SZJs,47
19
19
  anydi/ext/django/__init__.py,sha256=QI1IABCVgSDTUoh7M9WMECKXwB3xvh04HfQ9TOWw1Mk,223
20
20
  anydi/ext/django/_container.py,sha256=cxVoYQG16WP0S_Yv4TnLwuaaT7NVEOhLWO-YdALJUb4,418
21
21
  anydi/ext/django/_settings.py,sha256=Z0RlAuXoO73oahWeMkK10w8c-4uCBde-DBpeKTV5USY,853
22
- anydi/ext/django/_utils.py,sha256=sYibOAo2ildVLppIM5EFI_SyGmJy4_IGJLuzKMYvLgs,3933
22
+ anydi/ext/django/_utils.py,sha256=N1DZlY5RHxbb7TUWH-BWWC78GzUX_a1oYEZ8hT8WgaE,2663
23
23
  anydi/ext/django/apps.py,sha256=mfOLUQ1i-B4e3f5NPitbRVuVUlm4QoFzuJVXsI8zGWA,2791
24
24
  anydi/ext/django/middleware.py,sha256=5OUdp0OWRozyW338Sq04BDhacaFlyUTTOduS_7EwCTA,854
25
25
  anydi/ext/django/ninja/__init__.py,sha256=4J0zoHPK9itbTVrjjvLX6Ftrsb2ND8bITqNDIJzEhks,520
@@ -27,8 +27,8 @@ anydi/ext/django/ninja/_operation.py,sha256=wk5EOjLY3KVIHk9jMCGsFsja9-dQmMOpLpHX
27
27
  anydi/ext/django/ninja/_signature.py,sha256=UVLmKpYvH1fNb9C7ffP50KCmh0BE6unHegfss5dvpVU,2261
28
28
  anydi/ext/starlette/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
29
29
  anydi/ext/starlette/middleware.py,sha256=MxnzshAs-CMvjJp0r457k52MzBL8O4KAuClnF6exBdU,803
30
- anydi-0.42.0.dist-info/METADATA,sha256=1Di-jvpUvC5xYuUb7T1GlcJbN3X0TepPAwaquSmc5HI,4957
31
- anydi-0.42.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
32
- anydi-0.42.0.dist-info/entry_points.txt,sha256=Nklo9f3Oe4AkNsEgC4g43nCJ-23QDngZSVDNRMdaILI,43
33
- anydi-0.42.0.dist-info/licenses/LICENSE,sha256=V6rU8a8fv6o2jQ-7ODHs0XfDFimot8Q6Km6CylRIDTo,1069
34
- anydi-0.42.0.dist-info/RECORD,,
30
+ anydi-0.43.0.dist-info/METADATA,sha256=QwACa2m9ugixipKP81nR-jASIqmk9c2W_Y3LNPYvW58,4957
31
+ anydi-0.43.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
32
+ anydi-0.43.0.dist-info/entry_points.txt,sha256=Nklo9f3Oe4AkNsEgC4g43nCJ-23QDngZSVDNRMdaILI,43
33
+ anydi-0.43.0.dist-info/licenses/LICENSE,sha256=V6rU8a8fv6o2jQ-7ODHs0XfDFimot8Q6Km6CylRIDTo,1069
34
+ anydi-0.43.0.dist-info/RECORD,,
File without changes