python-injection 0.23.3__tar.gz → 0.25.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.
Files changed (32) hide show
  1. {python_injection-0.23.3 → python_injection-0.25.0}/PKG-INFO +2 -1
  2. {python_injection-0.23.3 → python_injection-0.25.0}/injection/_core/common/type.py +2 -45
  3. {python_injection-0.23.3 → python_injection-0.25.0}/injection/_core/descriptors.py +1 -5
  4. {python_injection-0.23.3 → python_injection-0.25.0}/injection/_core/injectables.py +5 -4
  5. {python_injection-0.23.3 → python_injection-0.25.0}/injection/_core/module.py +96 -76
  6. {python_injection-0.23.3 → python_injection-0.25.0}/injection/_core/scope.py +4 -4
  7. python_injection-0.25.0/injection/testing/__init__.py +30 -0
  8. {python_injection-0.23.3 → python_injection-0.25.0}/injection/testing/__init__.pyi +2 -0
  9. {python_injection-0.23.3 → python_injection-0.25.0}/pyproject.toml +8 -6
  10. python_injection-0.23.3/injection/testing/__init__.py +0 -29
  11. {python_injection-0.23.3 → python_injection-0.25.0}/.gitignore +0 -0
  12. {python_injection-0.23.3 → python_injection-0.25.0}/LICENSE +0 -0
  13. {python_injection-0.23.3 → python_injection-0.25.0}/README.md +0 -0
  14. {python_injection-0.23.3 → python_injection-0.25.0}/injection/__init__.py +0 -0
  15. {python_injection-0.23.3 → python_injection-0.25.0}/injection/__init__.pyi +0 -0
  16. {python_injection-0.23.3 → python_injection-0.25.0}/injection/_core/__init__.py +0 -0
  17. {python_injection-0.23.3 → python_injection-0.25.0}/injection/_core/asfunction.py +0 -0
  18. {python_injection-0.23.3 → python_injection-0.25.0}/injection/_core/common/__init__.py +0 -0
  19. {python_injection-0.23.3 → python_injection-0.25.0}/injection/_core/common/asynchronous.py +0 -0
  20. {python_injection-0.23.3 → python_injection-0.25.0}/injection/_core/common/event.py +0 -0
  21. {python_injection-0.23.3 → python_injection-0.25.0}/injection/_core/common/invertible.py +0 -0
  22. {python_injection-0.23.3 → python_injection-0.25.0}/injection/_core/common/key.py +0 -0
  23. {python_injection-0.23.3 → python_injection-0.25.0}/injection/_core/common/lazy.py +0 -0
  24. {python_injection-0.23.3 → python_injection-0.25.0}/injection/_core/common/threading.py +0 -0
  25. {python_injection-0.23.3 → python_injection-0.25.0}/injection/_core/slots.py +0 -0
  26. {python_injection-0.23.3 → python_injection-0.25.0}/injection/entrypoint.py +0 -0
  27. {python_injection-0.23.3 → python_injection-0.25.0}/injection/exceptions.py +0 -0
  28. {python_injection-0.23.3 → python_injection-0.25.0}/injection/ext/__init__.py +0 -0
  29. {python_injection-0.23.3 → python_injection-0.25.0}/injection/ext/fastapi.py +0 -0
  30. {python_injection-0.23.3 → python_injection-0.25.0}/injection/ext/fastapi.pyi +0 -0
  31. {python_injection-0.23.3 → python_injection-0.25.0}/injection/loaders.py +1 -1
  32. {python_injection-0.23.3 → python_injection-0.25.0}/injection/py.typed +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-injection
3
- Version: 0.23.3
3
+ Version: 0.25.0
4
4
  Summary: Fast and easy dependency injection framework.
5
5
  Project-URL: Repository, https://github.com/100nm/python-injection
6
6
  Author: remimd
@@ -23,6 +23,7 @@ Classifier: Topic :: Software Development :: Libraries :: Python Modules
23
23
  Classifier: Topic :: Software Development :: Testing
24
24
  Classifier: Typing :: Typed
25
25
  Requires-Python: <3.15,>=3.12
26
+ Requires-Dist: type-analyzer
26
27
  Provides-Extra: async
27
28
  Requires-Dist: anyio; extra == 'async'
28
29
  Description-Content-Type: text/markdown
@@ -10,19 +10,16 @@ from collections.abc import (
10
10
  Iterator,
11
11
  )
12
12
  from inspect import isclass, isfunction
13
- from types import GenericAlias, NoneType, UnionType
13
+ from types import GenericAlias, UnionType
14
14
  from typing import (
15
- Annotated,
16
15
  Any,
17
16
  TypeAliasType,
18
- Union,
19
17
  get_args,
20
18
  get_origin,
21
19
  get_type_hints,
22
20
  )
23
21
 
24
- type TypeDef[T] = type[T] | TypeAliasType | GenericAlias
25
- type InputType[T] = TypeDef[T] | UnionType
22
+ type InputType[T] = type[T] | TypeAliasType | GenericAlias | UnionType
26
23
  type TypeInfo[T] = (
27
24
  InputType[T]
28
25
  | Callable[..., T]
@@ -67,43 +64,3 @@ def get_yield_hint[T](
67
64
  return (arg,)
68
65
 
69
66
  return ()
70
-
71
-
72
- def standardize_types(
73
- *types: InputType[Any],
74
- with_origin: bool = False,
75
- ignore_none_type: bool = False,
76
- ) -> Iterator[TypeDef[Any]]:
77
- for tp in types:
78
- if tp is None or (ignore_none_type and tp is NoneType):
79
- continue
80
-
81
- origin = get_origin(tp)
82
-
83
- if origin is Union or isinstance(tp, UnionType):
84
- inner_types = get_args(tp)
85
-
86
- elif origin is Annotated:
87
- inner_types = get_args(tp)[:1]
88
-
89
- else:
90
- yield tp
91
-
92
- if with_origin:
93
- if origin is not None:
94
- yield origin
95
-
96
- for alias in (tp, origin):
97
- if isinstance(alias, TypeAliasType):
98
- yield from standardize_types(
99
- alias.__value__,
100
- with_origin=with_origin,
101
- )
102
-
103
- continue
104
-
105
- yield from standardize_types(
106
- *inner_types,
107
- with_origin=with_origin,
108
- ignore_none_type=ignore_none_type,
109
- )
@@ -53,11 +53,7 @@ class MappedScope:
53
53
  if name == descriptor_name:
54
54
  continue
55
55
 
56
- key = self.__module.reserve_scoped_slot(
57
- hint,
58
- scope_name=self.__name,
59
- ignore_none_type=True,
60
- )
56
+ key = self.__module.reserve_scoped_slot(hint, scope_name=self.__name)
61
57
  yield name, key
62
58
 
63
59
  def __mapping_from(self, instance: object) -> dict[SlotKey[Any], Any]:
@@ -122,11 +122,12 @@ class SingletonInjectable[T](Injectable[T]):
122
122
  class ScopedInjectable[R, T](Injectable[T], ABC):
123
123
  factory: Caller[..., R]
124
124
  scope_name: str
125
+ key: SlotKey[T] = field(default_factory=SlotKey)
125
126
  logic: CacheLogic[T] = field(default_factory=CacheLogic)
126
127
 
127
128
  @property
128
129
  def is_locked(self) -> bool:
129
- return in_scope_cache(self, self.scope_name)
130
+ return in_scope_cache(self.key, self.scope_name)
130
131
 
131
132
  @abstractmethod
132
133
  async def abuild(self, scope: Scope) -> T:
@@ -139,12 +140,12 @@ class ScopedInjectable[R, T](Injectable[T], ABC):
139
140
  async def aget_instance(self) -> T:
140
141
  scope = self.__get_scope()
141
142
  factory = partial(self.abuild, scope)
142
- return await self.logic.aget_or_create(scope.cache, self, factory)
143
+ return await self.logic.aget_or_create(scope.cache, self.key, factory)
143
144
 
144
145
  def get_instance(self) -> T:
145
146
  scope = self.__get_scope()
146
147
  factory = partial(self.build, scope)
147
- return self.logic.get_or_create(scope.cache, self, factory)
148
+ return self.logic.get_or_create(scope.cache, self.key, factory)
148
149
 
149
150
  def unlock(self) -> None:
150
151
  if self.is_locked:
@@ -187,7 +188,7 @@ class SimpleScopedInjectable[T](ScopedInjectable[T, T]):
187
188
  return self.factory.call()
188
189
 
189
190
  def unlock(self) -> None:
190
- remove_scoped_values(self, self.scope_name)
191
+ remove_scoped_values(self.key, self.scope_name)
191
192
 
192
193
 
193
194
  @dataclass(repr=False, eq=False, frozen=True, slots=True)
@@ -1,5 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import itertools
3
4
  from abc import ABC, abstractmethod
4
5
  from collections import OrderedDict, deque
5
6
  from collections.abc import (
@@ -42,6 +43,8 @@ from typing import (
42
43
  runtime_checkable,
43
44
  )
44
45
 
46
+ from type_analyzer import MatchingTypesConfig, iter_matching_types, matching_types
47
+
45
48
  from injection._core.common.asynchronous import (
46
49
  AsyncCaller,
47
50
  Caller,
@@ -55,11 +58,9 @@ from injection._core.common.lazy import Lazy, alazy, lazy
55
58
  from injection._core.common.threading import get_lock
56
59
  from injection._core.common.type import (
57
60
  InputType,
58
- TypeDef,
59
61
  TypeInfo,
60
62
  get_return_types,
61
63
  get_yield_hint,
62
- standardize_types,
63
64
  )
64
65
  from injection._core.injectables import (
65
66
  AsyncCMScopedInjectable,
@@ -118,15 +119,25 @@ class ModuleEventProxy(ModuleEvent):
118
119
  return f"`{self.module}` has propagated an event: {self.origin}"
119
120
 
120
121
  @property
121
- def history(self) -> Iterator[Event]:
122
- if isinstance(self.event, ModuleEventProxy):
123
- yield from self.event.history
124
-
125
- yield self.event
122
+ def is_duplicate(self) -> bool:
123
+ module, origin = self.module, self.origin
124
+ return any(
125
+ module is event.module and origin is event.origin
126
+ for event in self.proxy_history
127
+ )
126
128
 
127
129
  @property
128
130
  def origin(self) -> Event:
129
- return next(self.history)
131
+ reversed_proxy_history = reversed(tuple(self.proxy_history))
132
+ return next(reversed_proxy_history, self).event
133
+
134
+ @property
135
+ def proxy_history(self) -> Iterator[ModuleEventProxy]:
136
+ event = self.event
137
+
138
+ if isinstance(event, ModuleEventProxy):
139
+ yield event
140
+ yield from event.proxy_history
130
141
 
131
142
 
132
143
  @dataclass(frozen=True, slots=True)
@@ -160,8 +171,10 @@ class ModulePriorityUpdated(ModuleEvent):
160
171
 
161
172
  @dataclass(frozen=True, slots=True)
162
173
  class UnlockCalled(Event):
174
+ module: Module
175
+
163
176
  def __str__(self) -> str:
164
- return "An `unlock` method has been called."
177
+ return f"`{self.module}.unlock` has been called."
165
178
 
166
179
 
167
180
  """
@@ -229,27 +242,10 @@ class Updater[T]:
229
242
  classes: Iterable[InputType[T]]
230
243
  injectable: Injectable[T]
231
244
  mode: Mode
232
- ignore_none_type: bool
233
245
 
234
246
  def make_record(self) -> Record[T]:
235
247
  return Record(self.injectable, self.mode)
236
248
 
237
- @classmethod
238
- def with_basics(
239
- cls,
240
- on: TypeInfo[T],
241
- /,
242
- injectable: Injectable[T],
243
- mode: Mode | ModeStr,
244
- ignore_none_type: bool = False,
245
- ) -> Self:
246
- return cls(
247
- classes=get_return_types(on),
248
- injectable=injectable,
249
- mode=Mode(mode),
250
- ignore_none_type=ignore_none_type,
251
- )
252
-
253
249
 
254
250
  @dataclass(repr=False, frozen=True, slots=True)
255
251
  class Locator(Broker):
@@ -263,21 +259,15 @@ class Locator(Broker):
263
259
  )
264
260
 
265
261
  def __getitem__[T](self, cls: InputType[T], /) -> Injectable[T]:
266
- for input_class in self.__standardize_inputs((cls,)):
267
- try:
268
- record = self.__records[input_class]
269
- except KeyError:
270
- continue
271
-
262
+ try:
263
+ record = self.__records[cls]
264
+ except KeyError as exc:
265
+ raise NoInjectable(cls) from exc
266
+ else:
272
267
  return record.injectable
273
268
 
274
- raise NoInjectable(cls)
275
-
276
269
  def __contains__(self, cls: InputType[Any], /) -> bool:
277
- return any(
278
- input_class in self.__records
279
- for input_class in self.__standardize_inputs((cls,))
280
- )
270
+ return cls in self.__records
281
271
 
282
272
  @property
283
273
  def is_locked(self) -> bool:
@@ -289,8 +279,7 @@ class Locator(Broker):
289
279
 
290
280
  def update[T](self, updater: Updater[T]) -> Self:
291
281
  record = updater.make_record()
292
- classes = self.__reduce_classes(updater)
293
- records = dict(self.__prepare_for_updating(classes, record))
282
+ records = dict(self.__prepare_for_updating(updater.classes, record))
294
283
 
295
284
  if records:
296
285
  event = LocatorDependenciesUpdated(self, records.keys(), record.mode)
@@ -351,21 +340,6 @@ class Locator(Broker):
351
340
 
352
341
  return new_mode.rank > existing_mode.rank
353
342
 
354
- @staticmethod
355
- def __reduce_classes[T](updater: Updater[T]) -> frozenset[TypeDef[T]]:
356
- return frozenset(
357
- standardize_types(
358
- *updater.classes,
359
- ignore_none_type=updater.ignore_none_type,
360
- )
361
- )
362
-
363
- @staticmethod
364
- def __standardize_inputs[T](
365
- classes: Iterable[InputType[T]],
366
- ) -> Iterator[InputType[T]]:
367
- return standardize_types(*classes, with_origin=True)
368
-
369
343
 
370
344
  """
371
345
  Module
@@ -422,23 +396,26 @@ class Module(Broker, EventListener):
422
396
  self.__locator.add_listener(self)
423
397
 
424
398
  def __getitem__[T](self, cls: InputType[T], /) -> Injectable[T]:
425
- for broker in self.__brokers:
426
- with suppress(KeyError):
427
- return broker[cls]
399
+ key_types = self.__matching_key_types(cls)
400
+
401
+ for broker in self._iter_brokers():
402
+ for key_type in key_types:
403
+ with suppress(KeyError):
404
+ return broker[key_type]
428
405
 
429
406
  raise NoInjectable(cls)
430
407
 
431
408
  def __contains__(self, cls: InputType[Any], /) -> bool:
432
- return any(cls in broker for broker in self.__brokers)
409
+ key_types = self.__matching_key_types(cls)
410
+ return any(
411
+ key_type in broker
412
+ for broker in self._iter_brokers()
413
+ for key_type in key_types
414
+ )
433
415
 
434
416
  @property
435
417
  def is_locked(self) -> bool:
436
- return any(broker.is_locked for broker in self.__brokers)
437
-
438
- @property
439
- def __brokers(self) -> Iterator[Broker]:
440
- yield from self.__modules
441
- yield self.__locator
418
+ return any(broker.is_locked for broker in self._iter_brokers())
442
419
 
443
420
  def injectable[**P, T](
444
421
  self,
@@ -455,8 +432,7 @@ class Module(Broker, EventListener):
455
432
  factory = extract_caller(self.make_injected_function(wp) if inject else wp)
456
433
  injectable = cls(factory) # type: ignore[arg-type]
457
434
  hints = on if ignore_type_hint else (wp, on)
458
- updater = Updater.with_basics(hints, injectable, mode)
459
- self.update(updater)
435
+ self.update_from(hints, injectable, mode)
460
436
  return wp
461
437
 
462
438
  return decorator(wrapped) if wrapped else decorator
@@ -507,8 +483,7 @@ class Module(Broker, EventListener):
507
483
  def should_be_injectable[T](self, wrapped: type[T] | None = None, /) -> Any:
508
484
  def decorator(wp: type[T]) -> type[T]:
509
485
  injectable = ShouldBeInjectable(wp)
510
- updater = Updater.with_basics(wp, injectable, Mode.FALLBACK)
511
- self.update(updater)
486
+ self.update_from(wp, injectable, Mode.FALLBACK)
512
487
  return wp
513
488
 
514
489
  return decorator(wrapped) if wrapped else decorator
@@ -564,11 +539,9 @@ class Module(Broker, EventListener):
564
539
  scope_name: str,
565
540
  *,
566
541
  mode: Mode | ModeStr = Mode.get_default(),
567
- ignore_none_type: bool = False,
568
542
  ) -> SlotKey[T]:
569
543
  injectable = ScopedSlotInjectable(cls, scope_name)
570
- updater = Updater.with_basics(cls, injectable, mode, ignore_none_type)
571
- self.update(updater)
544
+ self.update_from(cls, injectable, mode)
572
545
  return injectable.key
573
546
 
574
547
  def inject[**P, T](
@@ -791,6 +764,21 @@ class Module(Broker, EventListener):
791
764
  self.__locator.update(updater)
792
765
  return self
793
766
 
767
+ def update_from[T](
768
+ self,
769
+ on: TypeInfo[T],
770
+ /,
771
+ injectable: Injectable[T],
772
+ mode: Mode | ModeStr,
773
+ ) -> Self:
774
+ updater = Updater(
775
+ classes=self.__build_key_types(on),
776
+ injectable=injectable,
777
+ mode=Mode(mode),
778
+ )
779
+ self.update(updater)
780
+ return self
781
+
794
782
  def init_modules(self, *modules: Module) -> Self:
795
783
  for module in tuple(self.__modules):
796
784
  self.stop_using(module)
@@ -860,7 +848,7 @@ class Module(Broker, EventListener):
860
848
  return self
861
849
 
862
850
  def unlock(self) -> Self:
863
- event = UnlockCalled()
851
+ event = UnlockCalled(self)
864
852
 
865
853
  with self.dispatch(event, lock_bypass=True):
866
854
  self.unsafe_unlocking()
@@ -868,11 +856,11 @@ class Module(Broker, EventListener):
868
856
  return self
869
857
 
870
858
  def unsafe_unlocking(self) -> None:
871
- for broker in self.__brokers:
859
+ for broker in self._iter_brokers():
872
860
  broker.unsafe_unlocking()
873
861
 
874
862
  async def all_ready(self) -> None:
875
- for broker in self.__brokers:
863
+ for broker in self._iter_brokers():
876
864
  await broker.all_ready()
877
865
 
878
866
  def add_logger(self, logger: Logger) -> Self:
@@ -887,8 +875,12 @@ class Module(Broker, EventListener):
887
875
  self.__channel.remove_listener(listener)
888
876
  return self
889
877
 
890
- def on_event(self, event: Event, /) -> ContextManager[None]:
878
+ def on_event(self, event: Event, /) -> ContextManager[None] | None:
891
879
  self_event = ModuleEventProxy(self, event)
880
+
881
+ if self_event.is_duplicate:
882
+ return None
883
+
892
884
  return self.dispatch(self_event)
893
885
 
894
886
  @contextmanager
@@ -902,6 +894,20 @@ class Module(Broker, EventListener):
902
894
  finally:
903
895
  self.__debug(event)
904
896
 
897
+ def _iter_brokers(self, visited: set[Module] | None = None, /) -> Iterator[Broker]:
898
+ if visited is None:
899
+ visited = set()
900
+
901
+ if self in visited:
902
+ return
903
+
904
+ visited.add(self)
905
+
906
+ for module in self.__modules:
907
+ yield from module._iter_brokers(visited)
908
+
909
+ yield self.__locator
910
+
905
911
  def __debug(self, message: object) -> None:
906
912
  for logger in self.__loggers:
907
913
  logger.debug(message)
@@ -933,6 +939,20 @@ class Module(Broker, EventListener):
933
939
  def default(cls) -> Module:
934
940
  return cls.from_name("__default__")
935
941
 
942
+ @staticmethod
943
+ def __build_key_types(input_cls: Any) -> frozenset[Any]:
944
+ config = MatchingTypesConfig(ignore_none=True)
945
+ return frozenset(
946
+ itertools.chain.from_iterable(
947
+ iter_matching_types(cls, config) for cls in get_return_types(input_cls)
948
+ )
949
+ )
950
+
951
+ @staticmethod
952
+ def __matching_key_types(input_cls: Any) -> tuple[Any, ...]:
953
+ config = MatchingTypesConfig(with_origin=True, with_type_alias_value=True)
954
+ return matching_types(input_cls, config)
955
+
936
956
 
937
957
  def mod(name: str | None = None, /) -> Module:
938
958
  if name is None:
@@ -182,11 +182,11 @@ def get_scope[T](name: str, default: T | EllipsisType = ...) -> Scope | T:
182
182
  return default
183
183
 
184
184
 
185
- def in_scope_cache(key: Any, scope_name: str) -> bool:
185
+ def in_scope_cache(key: SlotKey[Any], scope_name: str) -> bool:
186
186
  return any(key in scope.cache for scope in get_active_scopes(scope_name))
187
187
 
188
188
 
189
- def remove_scoped_values(key: Any, scope_name: str) -> None:
189
+ def remove_scoped_values(key: SlotKey[Any], scope_name: str) -> None:
190
190
  for scope in get_active_scopes(scope_name):
191
191
  scope.cache.pop(key, None)
192
192
 
@@ -233,7 +233,7 @@ def _bind_scope(
233
233
  class Scope(Protocol):
234
234
  __slots__ = ()
235
235
 
236
- cache: MutableMapping[Any, Any]
236
+ cache: MutableMapping[SlotKey[Any], Any]
237
237
 
238
238
  @abstractmethod
239
239
  async def aenter[T](self, context_manager: AsyncContextManager[T]) -> T:
@@ -247,7 +247,7 @@ class Scope(Protocol):
247
247
  @dataclass(repr=False, frozen=True, slots=True)
248
248
  class BaseScope[T](Scope, ABC):
249
249
  delegate: T
250
- cache: MutableMapping[Any, Any] = field(
250
+ cache: MutableMapping[SlotKey[Any], Any] = field(
251
251
  default_factory=dict,
252
252
  init=False,
253
253
  hash=False,
@@ -0,0 +1,30 @@
1
+ from typing import Final
2
+
3
+ from injection import mod
4
+ from injection.loaders import LoadedProfile, ProfileLoader, load_profile
5
+
6
+ __all__ = (
7
+ "TEST_PROFILE_NAME",
8
+ "load_test_profile",
9
+ "reserve_scoped_test_slot",
10
+ "set_test_constant",
11
+ "should_be_test_injectable",
12
+ "test_constant",
13
+ "test_injectable",
14
+ "test_scoped",
15
+ "test_singleton",
16
+ )
17
+
18
+ TEST_PROFILE_NAME: Final[str] = "__testing__"
19
+
20
+ reserve_scoped_test_slot = mod(TEST_PROFILE_NAME).reserve_scoped_slot
21
+ set_test_constant = mod(TEST_PROFILE_NAME).set_constant
22
+ should_be_test_injectable = mod(TEST_PROFILE_NAME).should_be_injectable
23
+ test_constant = mod(TEST_PROFILE_NAME).constant
24
+ test_injectable = mod(TEST_PROFILE_NAME).injectable
25
+ test_scoped = mod(TEST_PROFILE_NAME).scoped
26
+ test_singleton = mod(TEST_PROFILE_NAME).singleton
27
+
28
+
29
+ def load_test_profile(loader: ProfileLoader | None = None) -> LoadedProfile:
30
+ return load_profile(TEST_PROFILE_NAME, loader)
@@ -3,6 +3,8 @@ from typing import Final
3
3
  from injection import Module
4
4
  from injection.loaders import LoadedProfile, ProfileLoader
5
5
 
6
+ TEST_PROFILE_NAME: Final[str] = ...
7
+
6
8
  __MODULE: Final[Module] = ...
7
9
 
8
10
  reserve_scoped_test_slot = __MODULE.reserve_scoped_slot
@@ -30,7 +30,7 @@ test = [
30
30
 
31
31
  [project]
32
32
  name = "python-injection"
33
- version = "0.23.3"
33
+ version = "0.25.0"
34
34
  description = "Fast and easy dependency injection framework."
35
35
  license = "MIT"
36
36
  license-files = ["LICENSE"]
@@ -55,7 +55,9 @@ classifiers = [
55
55
  "Natural Language :: English",
56
56
  "Typing :: Typed",
57
57
  ]
58
- dependencies = []
58
+ dependencies = [
59
+ "type-analyzer",
60
+ ]
59
61
 
60
62
  [project.optional-dependencies]
61
63
  async = [
@@ -106,12 +108,12 @@ init_forbid_extra = true
106
108
  init_typed = true
107
109
  warn_required_dynamic_aliases = true
108
110
 
109
- [tool.pytest.ini_options]
110
- python_files = "test_*.py"
111
- addopts = "--tb short --cov ./ --cov-report term-missing:skip-covered"
111
+ [tool.pytest]
112
+ python_files = ["test_*.py"]
113
+ addopts = ["--tb", "short", "--cov", "./", "--cov-report", "term-missing:skip-covered"]
112
114
  asyncio_default_fixture_loop_scope = "session"
113
115
  asyncio_mode = "auto"
114
- testpaths = "**/tests/"
116
+ testpaths = ["**/tests/"]
115
117
 
116
118
  [tool.ruff]
117
119
  line-length = 88
@@ -1,29 +0,0 @@
1
- from typing import Final
2
-
3
- from injection import mod
4
- from injection.loaders import LoadedProfile, ProfileLoader, load_profile
5
-
6
- __all__ = (
7
- "load_test_profile",
8
- "reserve_scoped_test_slot",
9
- "set_test_constant",
10
- "should_be_test_injectable",
11
- "test_constant",
12
- "test_injectable",
13
- "test_scoped",
14
- "test_singleton",
15
- )
16
-
17
- _TEST_PROFILE_NAME: Final[str] = "__testing__"
18
-
19
- reserve_scoped_test_slot = mod(_TEST_PROFILE_NAME).reserve_scoped_slot
20
- set_test_constant = mod(_TEST_PROFILE_NAME).set_constant
21
- should_be_test_injectable = mod(_TEST_PROFILE_NAME).should_be_injectable
22
- test_constant = mod(_TEST_PROFILE_NAME).constant
23
- test_injectable = mod(_TEST_PROFILE_NAME).injectable
24
- test_scoped = mod(_TEST_PROFILE_NAME).scoped
25
- test_singleton = mod(_TEST_PROFILE_NAME).singleton
26
-
27
-
28
- def load_test_profile(loader: ProfileLoader | None = None) -> LoadedProfile:
29
- return load_profile(_TEST_PROFILE_NAME, loader)
@@ -176,12 +176,12 @@ class ProfileLoader:
176
176
 
177
177
  def __init_subsets_for(self, module: Module) -> Module:
178
178
  if not self.__is_empty and not self.__is_initialized(module):
179
+ self.__mark_initialized(module)
179
180
  target_modules = tuple(
180
181
  self.__init_subsets_for(mod(name))
181
182
  for name in self.module_subsets.get(module.name, ())
182
183
  )
183
184
  module.init_modules(*target_modules)
184
- self.__mark_initialized(module)
185
185
 
186
186
  return module
187
187