python-injection 0.13.1__py3-none-any.whl → 0.14.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.
injection/__init__.pyi CHANGED
@@ -74,11 +74,19 @@ class Module:
74
74
  def __contains__(self, cls: _InputType[Any], /) -> bool: ...
75
75
  @property
76
76
  def is_locked(self) -> bool: ...
77
- def inject[**P, T](self, wrapped: Callable[P, T] = ..., /) -> Any:
77
+ def inject[**P, T](
78
+ self,
79
+ wrapped: Callable[P, T] = ...,
80
+ /,
81
+ *,
82
+ threadsafe: bool = ...,
83
+ ) -> Any:
78
84
  """
79
85
  Decorator applicable to a class or function. Inject function dependencies using
80
86
  parameter type annotations. If applied to a class, the dependencies resolved
81
87
  will be those of the `__init__` method.
88
+
89
+ With `threadsafe=True`, the injection logic is wrapped in a `threading.Lock`.
82
90
  """
83
91
 
84
92
  def injectable[**P, T](
@@ -166,6 +174,7 @@ class Module:
166
174
  self,
167
175
  wrapped: Callable[P, T],
168
176
  /,
177
+ threadsafe: bool = ...,
169
178
  ) -> Callable[P, T]: ...
170
179
  async def afind_instance[T](self, cls: _InputType[T]) -> T: ...
171
180
  def find_instance[T](self, cls: _InputType[T]) -> T:
@@ -295,6 +304,8 @@ class Module:
295
304
  Function to unlock the module by deleting cached instances of singletons.
296
305
  """
297
306
 
307
+ @contextmanager
308
+ def load_profile(self, *names: str) -> Iterator[None]: ...
298
309
  async def all_ready(self) -> None: ...
299
310
  def add_logger(self, logger: Logger) -> Self: ...
300
311
  @classmethod
@@ -1,17 +1,7 @@
1
- import asyncio
2
1
  from abc import abstractmethod
3
- from collections.abc import Awaitable, Callable, Coroutine, Generator
2
+ from collections.abc import Awaitable, Callable, Generator
4
3
  from dataclasses import dataclass
5
- from typing import Any, Protocol, runtime_checkable
6
-
7
-
8
- def run_sync[T](coroutine: Coroutine[Any, Any, T]) -> T:
9
- loop = asyncio.get_event_loop()
10
-
11
- try:
12
- return loop.run_until_complete(coroutine)
13
- finally:
14
- coroutine.close()
4
+ from typing import Any, NoReturn, Protocol, runtime_checkable
15
5
 
16
6
 
17
7
  @dataclass(repr=False, eq=False, frozen=True, slots=True)
@@ -37,13 +27,13 @@ class Caller[**P, T](Protocol):
37
27
 
38
28
  @dataclass(repr=False, eq=False, frozen=True, slots=True)
39
29
  class AsyncCaller[**P, T](Caller[P, T]):
40
- callable: Callable[P, Coroutine[Any, Any, T]]
30
+ callable: Callable[P, Awaitable[T]]
41
31
 
42
32
  async def acall(self, /, *args: P.args, **kwargs: P.kwargs) -> T:
43
33
  return await self.callable(*args, **kwargs)
44
34
 
45
- def call(self, /, *args: P.args, **kwargs: P.kwargs) -> T:
46
- return run_sync(self.callable(*args, **kwargs))
35
+ def call(self, /, *args: P.args, **kwargs: P.kwargs) -> NoReturn:
36
+ raise RuntimeError("Can't call async callable synchronously.")
47
37
 
48
38
 
49
39
  @dataclass(repr=False, eq=False, frozen=True, slots=True)
@@ -1,56 +1,55 @@
1
- from collections.abc import Callable, Iterator, Mapping
2
- from types import MappingProxyType
1
+ from collections.abc import AsyncIterator, Awaitable, Callable, Iterator
2
+ from functools import partial
3
3
 
4
- from injection._core.common.invertible import Invertible
4
+ from injection._core.common.asynchronous import SimpleAwaitable
5
+ from injection._core.common.invertible import Invertible, SimpleInvertible
5
6
 
6
7
 
7
- class Lazy[T](Invertible[T]):
8
- __slots__ = ("__iterator", "__is_set")
8
+ def lazy[T](factory: Callable[..., T]) -> Invertible[T]:
9
+ def cache() -> Iterator[T]:
10
+ nonlocal factory
11
+ value = factory()
12
+ del factory
9
13
 
10
- __iterator: Iterator[T]
11
- __is_set: bool
14
+ while True:
15
+ yield value
12
16
 
13
- def __init__(self, factory: Callable[..., T]) -> None:
14
- self.__setup_cache(factory)
15
-
16
- def __invert__(self) -> T:
17
- return next(self.__iterator)
18
-
19
- @property
20
- def is_set(self) -> bool:
21
- return self.__is_set
17
+ getter = partial(next, cache())
18
+ return SimpleInvertible(getter)
22
19
 
23
- def __setup_cache(self, factory: Callable[..., T]) -> None:
24
- def infinite_yield() -> Iterator[T]:
25
- nonlocal factory
26
- cached = factory()
27
- self.__is_set = True
28
- del factory
29
20
 
30
- while True:
31
- yield cached
21
+ def alazy[T](factory: Callable[..., Awaitable[T]]) -> Awaitable[T]:
22
+ async def cache() -> AsyncIterator[T]:
23
+ nonlocal factory
24
+ value = await factory()
25
+ del factory
32
26
 
33
- self.__iterator = infinite_yield()
34
- self.__is_set = False
27
+ while True:
28
+ yield value
35
29
 
30
+ getter = partial(anext, cache())
31
+ return SimpleAwaitable(getter)
36
32
 
37
- class LazyMapping[K, V](Mapping[K, V]):
38
- __slots__ = ("__lazy",)
39
33
 
40
- __lazy: Lazy[Mapping[K, V]]
34
+ class Lazy[T](Invertible[T]):
35
+ __slots__ = ("__invertible", "__is_set")
41
36
 
42
- def __init__(self, iterator: Iterator[tuple[K, V]]) -> None:
43
- self.__lazy = Lazy(lambda: MappingProxyType(dict(iterator)))
37
+ __invertible: Invertible[T]
38
+ __is_set: bool
44
39
 
45
- def __getitem__(self, key: K, /) -> V:
46
- return (~self.__lazy)[key]
40
+ def __init__(self, factory: Callable[..., T]) -> None:
41
+ @lazy
42
+ def invertible() -> T:
43
+ value = factory()
44
+ self.__is_set = True
45
+ return value
47
46
 
48
- def __iter__(self) -> Iterator[K]:
49
- yield from ~self.__lazy
47
+ self.__invertible = invertible
48
+ self.__is_set = False
50
49
 
51
- def __len__(self) -> int:
52
- return len(~self.__lazy)
50
+ def __invert__(self) -> T:
51
+ return ~self.__invertible
53
52
 
54
53
  @property
55
54
  def is_set(self) -> bool:
56
- return self.__lazy.is_set
55
+ return self.__is_set
@@ -12,7 +12,7 @@ from typing import (
12
12
  runtime_checkable,
13
13
  )
14
14
 
15
- from injection._core.common.asynchronous import Caller, run_sync
15
+ from injection._core.common.asynchronous import Caller
16
16
  from injection._core.scope import Scope, get_active_scopes, get_scope
17
17
  from injection.exceptions import InjectionError
18
18
 
@@ -138,8 +138,8 @@ class AsyncCMScopedInjectable[T](ScopedInjectable[AsyncContextManager[T], T]):
138
138
  cm = await self.factory.acall()
139
139
  return await scope.aenter(cm)
140
140
 
141
- def build(self, scope: Scope) -> T:
142
- return run_sync(self.abuild(scope))
141
+ def build(self, scope: Scope) -> NoReturn:
142
+ raise RuntimeError("Can't use async context manager synchronously.")
143
143
 
144
144
 
145
145
  class CMScopedInjectable[T](ScopedInjectable[ContextManager[T], T]):
injection/_core/module.py CHANGED
@@ -1,8 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
- import asyncio
4
3
  from abc import ABC, abstractmethod
5
- from collections import OrderedDict
4
+ from collections import OrderedDict, deque
6
5
  from collections.abc import (
7
6
  AsyncIterator,
8
7
  Awaitable,
@@ -12,7 +11,7 @@ from collections.abc import (
12
11
  Iterator,
13
12
  Mapping,
14
13
  )
15
- from contextlib import asynccontextmanager, contextmanager, suppress
14
+ from contextlib import asynccontextmanager, contextmanager, nullcontext, suppress
16
15
  from dataclasses import dataclass, field
17
16
  from enum import StrEnum
18
17
  from functools import partialmethod, singledispatchmethod, update_wrapper
@@ -26,7 +25,7 @@ from inspect import (
26
25
  )
27
26
  from inspect import signature as inspect_signature
28
27
  from logging import Logger, getLogger
29
- from queue import Empty, Queue
28
+ from threading import Lock
30
29
  from types import MethodType
31
30
  from typing import (
32
31
  Any,
@@ -50,7 +49,7 @@ from injection._core.common.asynchronous import (
50
49
  from injection._core.common.event import Event, EventChannel, EventListener
51
50
  from injection._core.common.invertible import Invertible, SimpleInvertible
52
51
  from injection._core.common.key import new_short_key
53
- from injection._core.common.lazy import Lazy, LazyMapping
52
+ from injection._core.common.lazy import Lazy, alazy, lazy
54
53
  from injection._core.common.type import (
55
54
  InputType,
56
55
  TypeInfo,
@@ -296,6 +295,9 @@ class Locator(Broker):
296
295
 
297
296
  async def all_ready(self) -> None:
298
297
  for injectable in self.__injectables:
298
+ if injectable.is_locked:
299
+ continue
300
+
299
301
  await injectable.aget_instance()
300
302
 
301
303
  def add_listener(self, listener: EventListener) -> Self:
@@ -511,7 +513,7 @@ class Module(Broker, EventListener):
511
513
  mode: Mode | ModeStr = Mode.get_default(),
512
514
  ) -> Any:
513
515
  def decorator(wp: type[T]) -> type[T]:
514
- lazy_instance = Lazy(wp)
516
+ lazy_instance = lazy(wp)
515
517
  self.injectable(
516
518
  lambda: ~lazy_instance,
517
519
  ignore_type_hint=True,
@@ -541,13 +543,19 @@ class Module(Broker, EventListener):
541
543
  )
542
544
  return self
543
545
 
544
- def inject[**P, T](self, wrapped: Callable[P, T] | None = None, /) -> Any:
546
+ def inject[**P, T](
547
+ self,
548
+ wrapped: Callable[P, T] | None = None,
549
+ /,
550
+ *,
551
+ threadsafe: bool = False,
552
+ ) -> Any:
545
553
  def decorator(wp: Callable[P, T]) -> Callable[P, T]:
546
554
  if isclass(wp):
547
- wp.__init__ = self.inject(wp.__init__)
555
+ wp.__init__ = self.inject(wp.__init__, threadsafe=threadsafe)
548
556
  return wp
549
557
 
550
- return self.make_injected_function(wp)
558
+ return self.make_injected_function(wp, threadsafe)
551
559
 
552
560
  return decorator(wrapped) if wrapped else decorator
553
561
 
@@ -556,6 +564,7 @@ class Module(Broker, EventListener):
556
564
  self,
557
565
  wrapped: Callable[P, T],
558
566
  /,
567
+ threadsafe: bool = ...,
559
568
  ) -> SyncInjectedFunction[P, T]: ...
560
569
 
561
570
  @overload
@@ -563,12 +572,13 @@ class Module(Broker, EventListener):
563
572
  self,
564
573
  wrapped: Callable[P, Awaitable[T]],
565
574
  /,
575
+ threadsafe: bool = ...,
566
576
  ) -> AsyncInjectedFunction[P, T]: ...
567
577
 
568
- def make_injected_function(self, wrapped, /): # type: ignore[no-untyped-def]
569
- metadata = InjectMetadata(wrapped)
578
+ def make_injected_function(self, wrapped, /, threadsafe=False): # type: ignore[no-untyped-def]
579
+ metadata = InjectMetadata(wrapped, threadsafe)
570
580
 
571
- @metadata.on_setup
581
+ @metadata.task
572
582
  def listen() -> None:
573
583
  metadata.update(self)
574
584
  self.add_listener(metadata)
@@ -646,12 +656,10 @@ class Module(Broker, EventListener):
646
656
 
647
657
  def aget_lazy_instance(self, cls, default=None, *, cache=False): # type: ignore[no-untyped-def]
648
658
  if cache:
649
- coroutine = self.aget_instance(cls, default)
650
- return asyncio.ensure_future(coroutine)
659
+ return alazy(lambda: self.aget_instance(cls, default))
651
660
 
652
661
  function = self.make_injected_function(lambda instance=default: instance)
653
- metadata = function.__inject_metadata__
654
- metadata.set_owner(cls)
662
+ metadata = function.__inject_metadata__.set_owner(cls)
655
663
  return SimpleAwaitable(metadata.acall)
656
664
 
657
665
  @overload
@@ -674,11 +682,10 @@ class Module(Broker, EventListener):
674
682
 
675
683
  def get_lazy_instance(self, cls, default=None, *, cache=False): # type: ignore[no-untyped-def]
676
684
  if cache:
677
- return Lazy(lambda: self.get_instance(cls, default))
685
+ return lazy(lambda: self.get_instance(cls, default))
678
686
 
679
687
  function = self.make_injected_function(lambda instance=default: instance)
680
- metadata = function.__inject_metadata__
681
- metadata.set_owner(cls)
688
+ metadata = function.__inject_metadata__.set_owner(cls)
682
689
  return SimpleInvertible(metadata.call)
683
690
 
684
691
  def update[T](self, updater: Updater[T]) -> Self:
@@ -755,6 +762,23 @@ class Module(Broker, EventListener):
755
762
 
756
763
  return self
757
764
 
765
+ def load_profile(self, *names: str) -> ContextManager[None]:
766
+ modules = tuple(self.from_name(name) for name in names)
767
+
768
+ for module in modules:
769
+ module.unlock()
770
+
771
+ self.unlock().init_modules(*modules)
772
+
773
+ del module, modules
774
+
775
+ @contextmanager
776
+ def cleaner() -> Iterator[None]:
777
+ yield
778
+ self.unlock().init_modules()
779
+
780
+ return cleaner()
781
+
758
782
  async def all_ready(self) -> None:
759
783
  for broker in self.__brokers:
760
784
  await broker.all_ready()
@@ -832,10 +856,7 @@ InjectedFunction
832
856
 
833
857
  @dataclass(repr=False, frozen=True, slots=True)
834
858
  class Dependencies:
835
- mapping: Mapping[str, Injectable[Any]]
836
-
837
- def __bool__(self) -> bool:
838
- return bool(self.mapping)
859
+ lazy_mapping: Lazy[Mapping[str, Injectable[Any]]]
839
860
 
840
861
  def __iter__(self) -> Iterator[tuple[str, Any]]:
841
862
  for name, injectable in self.mapping.items():
@@ -849,10 +870,11 @@ class Dependencies:
849
870
 
850
871
  @property
851
872
  def are_resolved(self) -> bool:
852
- if isinstance(self.mapping, LazyMapping) and not self.mapping.is_set:
853
- return False
873
+ return self.lazy_mapping.is_set
854
874
 
855
- return bool(self)
875
+ @property
876
+ def mapping(self) -> Mapping[str, Injectable[Any]]:
877
+ return ~self.lazy_mapping
856
878
 
857
879
  async def aget_arguments(self) -> dict[str, Any]:
858
880
  return {key: value async for key, value in self}
@@ -861,12 +883,13 @@ class Dependencies:
861
883
  return dict(self)
862
884
 
863
885
  @classmethod
864
- def from_mapping(cls, mapping: Mapping[str, Injectable[Any]]) -> Self:
865
- return cls(mapping)
886
+ def from_iterable(cls, iterable: Iterable[tuple[str, Injectable[Any]]]) -> Self:
887
+ lazy_mapping = Lazy(lambda: dict(iterable))
888
+ return cls(lazy_mapping)
866
889
 
867
890
  @classmethod
868
891
  def empty(cls) -> Self:
869
- return cls.from_mapping({})
892
+ return cls.from_iterable(())
870
893
 
871
894
  @classmethod
872
895
  def resolve(
@@ -875,8 +898,8 @@ class Dependencies:
875
898
  module: Module,
876
899
  owner: type | None = None,
877
900
  ) -> Self:
878
- dependencies = LazyMapping(cls.__resolver(signature, module, owner))
879
- return cls.from_mapping(dependencies)
901
+ iterable = cls.__resolver(signature, module, owner)
902
+ return cls.from_iterable(iterable)
880
903
 
881
904
  @classmethod
882
905
  def __resolver(
@@ -916,22 +939,25 @@ class Arguments(NamedTuple):
916
939
  class InjectMetadata[**P, T](Caller[P, T], EventListener):
917
940
  __slots__ = (
918
941
  "__dependencies",
942
+ "__lock",
919
943
  "__owner",
920
- "__setup_queue",
921
944
  "__signature",
945
+ "__tasks",
922
946
  "__wrapped",
923
947
  )
924
948
 
925
949
  __dependencies: Dependencies
950
+ __lock: ContextManager[Any]
926
951
  __owner: type | None
927
- __setup_queue: Queue[Callable[..., Any]] | None
928
952
  __signature: Signature
953
+ __tasks: deque[Callable[..., Any]]
929
954
  __wrapped: Callable[P, T]
930
955
 
931
- def __init__(self, wrapped: Callable[P, T], /) -> None:
956
+ def __init__(self, wrapped: Callable[P, T], /, threadsafe: bool) -> None:
932
957
  self.__dependencies = Dependencies.empty()
958
+ self.__lock = Lock() if threadsafe else nullcontext()
933
959
  self.__owner = None
934
- self.__setup_queue = Queue()
960
+ self.__tasks = deque()
935
961
  self.__wrapped = wrapped
936
962
 
937
963
  @property
@@ -964,13 +990,17 @@ class InjectMetadata[**P, T](Caller[P, T], EventListener):
964
990
  return self.__bind(args, kwargs, additional_arguments)
965
991
 
966
992
  async def acall(self, /, *args: P.args, **kwargs: P.kwargs) -> T:
967
- self.__setup()
968
- arguments = await self.abind(args, kwargs)
993
+ with self.__lock:
994
+ self.__run_tasks()
995
+ arguments = await self.abind(args, kwargs)
996
+
969
997
  return self.wrapped(*arguments.args, **arguments.kwargs)
970
998
 
971
999
  def call(self, /, *args: P.args, **kwargs: P.kwargs) -> T:
972
- self.__setup()
973
- arguments = self.bind(args, kwargs)
1000
+ with self.__lock:
1001
+ self.__run_tasks()
1002
+ arguments = self.bind(args, kwargs)
1003
+
974
1004
  return self.wrapped(*arguments.args, **arguments.kwargs)
975
1005
 
976
1006
  def set_owner(self, owner: type) -> Self:
@@ -989,14 +1019,9 @@ class InjectMetadata[**P, T](Caller[P, T], EventListener):
989
1019
  self.__dependencies = Dependencies.resolve(self.signature, module, self.__owner)
990
1020
  return self
991
1021
 
992
- def on_setup[**_P, _T](self, wrapped: Callable[_P, _T] | None = None, /) -> Any:
1022
+ def task[**_P, _T](self, wrapped: Callable[_P, _T] | None = None, /) -> Any:
993
1023
  def decorator(wp: Callable[_P, _T]) -> Callable[_P, _T]:
994
- queue = self.__setup_queue
995
-
996
- if queue is None:
997
- raise RuntimeError(f"`{self}` is already up.")
998
-
999
- queue.put_nowait(wp)
1024
+ self.__tasks.append(wp)
1000
1025
  return wp
1001
1026
 
1002
1027
  return decorator(wrapped) if wrapped else decorator
@@ -1027,24 +1052,10 @@ class InjectMetadata[**P, T](Caller[P, T], EventListener):
1027
1052
  bound.arguments = bound.arguments | additional_arguments | bound.arguments
1028
1053
  return Arguments(bound.args, bound.kwargs)
1029
1054
 
1030
- def __close_setup_queue(self) -> None:
1031
- self.__setup_queue = None
1032
-
1033
- def __setup(self) -> None:
1034
- if (queue := self.__setup_queue) is None:
1035
- return
1036
-
1037
- while True:
1038
- try:
1039
- task = queue.get_nowait()
1040
- except Empty:
1041
- break
1042
-
1055
+ def __run_tasks(self) -> None:
1056
+ while tasks := self.__tasks:
1057
+ task = tasks.popleft()
1043
1058
  task()
1044
- queue.task_done()
1045
-
1046
- queue.join()
1047
- self.__close_setup_queue()
1048
1059
 
1049
1060
 
1050
1061
  class InjectedFunction[**P, T](ABC):
injection/_core/scope.py CHANGED
@@ -34,12 +34,12 @@ class _ScopeState:
34
34
  default_factory=lambda: ContextVar(f"scope@{new_short_key()}"),
35
35
  init=False,
36
36
  )
37
- __references: set[Scope] = field(
38
- default_factory=set,
37
+ __default: Scope | None = field(
38
+ default=None,
39
39
  init=False,
40
40
  )
41
- __shared_value: Scope | None = field(
42
- default=None,
41
+ __references: set[Scope] = field(
42
+ default_factory=set,
43
43
  init=False,
44
44
  )
45
45
 
@@ -47,8 +47,8 @@ class _ScopeState:
47
47
  def active_scopes(self) -> Iterator[Scope]:
48
48
  yield from self.__references
49
49
 
50
- if shared_value := self.__shared_value:
51
- yield shared_value
50
+ if default := self.__default:
51
+ yield default
52
52
 
53
53
  @contextmanager
54
54
  def bind_contextual_scope(self, scope: Scope) -> Iterator[None]:
@@ -69,15 +69,15 @@ class _ScopeState:
69
69
  "are defined on the same name."
70
70
  )
71
71
 
72
- self.__shared_value = scope
72
+ self.__default = scope
73
73
 
74
74
  try:
75
75
  yield
76
76
  finally:
77
- self.__shared_value = None
77
+ self.__default = None
78
78
 
79
79
  def get_scope(self) -> Scope | None:
80
- return self.__context_var.get(self.__shared_value)
80
+ return self.__context_var.get(self.__default)
81
81
 
82
82
 
83
83
  __SCOPES: Final[defaultdict[str, _ScopeState]] = defaultdict(_ScopeState)
@@ -125,11 +125,8 @@ def _bind_scope(name: str, scope: Scope, shared: bool) -> Iterator[None]:
125
125
  state.bind_shared_scope(scope) if shared else state.bind_contextual_scope(scope)
126
126
  )
127
127
 
128
- try:
129
- with strategy:
130
- yield
131
- finally:
132
- scope.cache.clear()
128
+ with strategy:
129
+ yield
133
130
 
134
131
 
135
132
  @runtime_checkable
@@ -201,9 +198,7 @@ class SyncScope(BaseScope[ExitStack]):
201
198
  return self.delegate.__exit__(exc_type, exc_value, traceback)
202
199
 
203
200
  async def aenter[T](self, context_manager: AsyncContextManager[T]) -> NoReturn:
204
- raise ScopeError(
205
- "Synchronous scope doesn't support asynchronous context manager."
206
- )
201
+ raise ScopeError("Synchronous scope doesn't support async context manager.")
207
202
 
208
203
  def enter[T](self, context_manager: ContextManager[T]) -> T:
209
204
  return self.delegate.enter_context(context_manager)
injection/utils.py CHANGED
@@ -1,5 +1,4 @@
1
- from collections.abc import Callable, Iterable, Iterator
2
- from contextlib import contextmanager
1
+ from collections.abc import Callable, Collection, Iterator
3
2
  from importlib import import_module
4
3
  from importlib.util import find_spec
5
4
  from pkgutil import walk_packages
@@ -18,26 +17,12 @@ def load_profile(*names: str) -> ContextManager[None]:
18
17
  A profile name is equivalent to an injection module name.
19
18
  """
20
19
 
21
- modules = tuple(mod(module_name) for module_name in names)
22
-
23
- for module in modules:
24
- module.unlock()
25
-
26
- target = mod().unlock().init_modules(*modules)
27
-
28
- del module, modules
29
-
30
- @contextmanager
31
- def cleaner() -> Iterator[None]:
32
- yield
33
- target.unlock().init_modules()
34
-
35
- return cleaner()
20
+ return mod().load_profile(*names)
36
21
 
37
22
 
38
23
  def load_modules_with_keywords(
39
24
  *packages: PythonModule | str,
40
- keywords: Iterable[str] | None = None,
25
+ keywords: Collection[str] | None = None,
41
26
  ) -> dict[str, PythonModule]:
42
27
  """
43
28
  Function to import modules from a Python package if one of the keywords is contained in the Python script.
@@ -54,16 +39,16 @@ def load_modules_with_keywords(
54
39
  f"import {injection_package_name}",
55
40
  )
56
41
 
57
- b_keywords = tuple(keyword.encode() for keyword in keywords)
58
-
59
42
  def predicate(module_name: str) -> bool:
60
- if (spec := find_spec(module_name)) and (module_path := spec.origin):
61
- with open(module_path, "rb") as script:
62
- for line in script:
63
- line = b" ".join(line.split(b" ")).strip()
43
+ spec = find_spec(module_name)
44
+
45
+ if spec and (module_path := spec.origin):
46
+ with open(module_path, "r") as file:
47
+ python_script = file.read()
64
48
 
65
- if line and any(keyword in line for keyword in b_keywords):
66
- return True
49
+ return bool(python_script) and any(
50
+ keyword in python_script for keyword in keywords
51
+ )
67
52
 
68
53
  return False
69
54
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-injection
3
- Version: 0.13.1
3
+ Version: 0.14.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
@@ -1,25 +1,25 @@
1
1
  injection/__init__.py,sha256=X0vIAoN4MDlhR7YIkup1qHgqbOkwZ3PWgdSi7h-udaM,1049
2
- injection/__init__.pyi,sha256=LXBL7_ihiw6NyU94xWGdXkWwxvzRBoLYBSOv88PV2WM,9640
2
+ injection/__init__.pyi,sha256=f3EhwOaJCnf37YZQdANvbUsAq360zn-62zFLNLJxvoQ,9916
3
3
  injection/exceptions.py,sha256=T__732aXxWWUz0sKc39ySteyolCS5tpqQC0oCnzUF2E,917
4
4
  injection/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
- injection/utils.py,sha256=SZBzmVAwRL-kilS2eRt9MCeNAL4zeaj_7Rm_2NLBzg0,3175
5
+ injection/utils.py,sha256=s4wwe98owmgVIkxzAg-mu9yvFISWOzz1AvF7eF7-2xY,2752
6
6
  injection/_core/__init__.py,sha256=XERocCxCZBxPGIaOR37yeiQOZyvjHQ6a4rgRmlkUSuU,1367
7
7
  injection/_core/descriptors.py,sha256=7fSHlgAqmgR_Uta8KocBapOt1Xyj2dI7RY9ZdoStTzw,726
8
8
  injection/_core/hook.py,sha256=Qv505pr3kjOE6UitftlLh9JKX9OCNqZBRtHbFha1gqM,3130
9
- injection/_core/injectables.py,sha256=SApYnP6vG3b1qi_KJx6hkJyPoyTmZfooxqPpOsxWSVI,4482
10
- injection/_core/module.py,sha256=wOoHW_bImPSwoxi3i8_rFXP_HWFJ_S78UuY_5EIilFQ,30595
11
- injection/_core/scope.py,sha256=TKOoxCmi2FIoXis0irReVNayDTte4KjLrTq8ZDcrOSk,5583
9
+ injection/_core/injectables.py,sha256=GIumNp0TXf8Voxe1sCPhcqq2gyw4E_hl7I45IJ_tyHE,4512
10
+ injection/_core/module.py,sha256=kotXKuOFv8Gt31J1R8nSgVnu-83_MhIV4B5Etu8_kzo,30968
11
+ injection/_core/scope.py,sha256=uZps_kQsXDL9tXRzF1ECK8ua3cdEXiIAxrrhPt8JNqg,5461
12
12
  injection/_core/common/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
13
- injection/_core/common/asynchronous.py,sha256=LlYMde_55osS3r8Sc3Fh5lgPp5UWmIfvyBzUeIDmMiM,1698
13
+ injection/_core/common/asynchronous.py,sha256=9bQDVRE6eqo9K0d5H9RzyFalf0WGoGP7cDrKDGbvZPI,1500
14
14
  injection/_core/common/event.py,sha256=XjzV8gxtGlGvzZs_ykvoC60qmdpd3RN08Eiqz5QUwes,1236
15
15
  injection/_core/common/invertible.py,sha256=YZlAdh6bNJgf1-74TRjwJTm8xrlgY95ZhOUGLSJ4XcY,482
16
16
  injection/_core/common/key.py,sha256=ghkZD-Y8Moz6SEPNgMh3xgsZUjDVq-XYAmXaCu5VuCA,80
17
- injection/_core/common/lazy.py,sha256=ZKx2O9CCFsF9F0SLM4zb7jSLksJUv-FBLCPlWltMN5k,1398
17
+ injection/_core/common/lazy.py,sha256=6xh5h0lmaNvl32V0WoX4VCTsNJ3zUJdWVqpLJ_YeIIU,1363
18
18
  injection/_core/common/type.py,sha256=QbBBhJp7i1p6gLzWX0TgofvfG7yDH-gHfEQcssVZeHo,2186
19
19
  injection/integrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
20
20
  injection/integrations/fastapi.py,sha256=YHSs85_3m6TUVtOwUcV157b3UZJQIw_aXWAg199a-YE,594
21
21
  injection/testing/__init__.py,sha256=ALcKuDYNdslmpgqotZzSWrXAW0kNNFUs9nzfO1ZgIGc,783
22
22
  injection/testing/__init__.pyi,sha256=6rv5NOYHEaiKMd82E1IIc8lFlLV9ttY57DLiMqGTXt8,482
23
- python_injection-0.13.1.dist-info/METADATA,sha256=y4ls59BE2tC-G8V8chV5dMl4um6zcLtYOejDr03Z3jQ,2996
24
- python_injection-0.13.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
25
- python_injection-0.13.1.dist-info/RECORD,,
23
+ python_injection-0.14.0.dist-info/METADATA,sha256=A25sC1rCj-HHSw-Sf57IIyHLhiPuDXUt53BdLBpKPnY,2996
24
+ python_injection-0.14.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
25
+ python_injection-0.14.0.dist-info/RECORD,,