python-injection 0.14.6.post1__py3-none-any.whl → 0.15.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.
@@ -6,6 +6,8 @@ from typing import Protocol, runtime_checkable
6
6
 
7
7
  @runtime_checkable
8
8
  class Invertible[T](Protocol):
9
+ __slots__ = ()
10
+
9
11
  @abstractmethod
10
12
  def __invert__(self) -> T:
11
13
  raise NotImplementedError
injection/_core/module.py CHANGED
@@ -1,5 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import threading
3
4
  from abc import ABC, abstractmethod
4
5
  from collections import OrderedDict, deque
5
6
  from collections.abc import (
@@ -27,7 +28,6 @@ from inspect import (
27
28
  )
28
29
  from inspect import signature as inspect_signature
29
30
  from logging import Logger, getLogger
30
- from threading import Lock
31
31
  from types import MethodType
32
32
  from typing import (
33
33
  Any,
@@ -982,7 +982,7 @@ class InjectMetadata[**P, T](Caller[P, T], EventListener):
982
982
 
983
983
  def __init__(self, wrapped: Callable[P, T], /, threadsafe: bool) -> None:
984
984
  self.__dependencies = Dependencies.empty()
985
- self.__lock = Lock() if threadsafe else nullcontext()
985
+ self.__lock = threading.Lock() if threadsafe else nullcontext()
986
986
  self.__owner = None
987
987
  self.__tasks = deque()
988
988
  self.__wrapped = wrapped
injection/_core/scope.py CHANGED
@@ -157,18 +157,18 @@ def get_scope(name, default=...): # type: ignore[no-untyped-def]
157
157
  def _bind_scope(name: str, scope: Scope, shared: bool) -> Iterator[None]:
158
158
  if shared:
159
159
  is_already_defined = bool(get_active_scopes(name))
160
- state = __SHARED_SCOPES[name]
160
+ states = __SHARED_SCOPES
161
161
 
162
162
  else:
163
163
  is_already_defined = bool(get_scope(name, default=None))
164
- state = __CONTEXTUAL_SCOPES[name]
164
+ states = __CONTEXTUAL_SCOPES
165
165
 
166
166
  if is_already_defined:
167
167
  raise ScopeAlreadyDefinedError(
168
168
  f"Scope `{name}` is already defined in the current context."
169
169
  )
170
170
 
171
- with state.bind(scope):
171
+ with states[name].bind(scope):
172
172
  yield
173
173
 
174
174
 
injection/utils.py CHANGED
@@ -1,15 +1,18 @@
1
+ import itertools
1
2
  import sys
2
- from collections.abc import Callable, Collection, Iterator
3
+ from collections.abc import Callable, Iterator, Mapping
4
+ from dataclasses import dataclass, field
3
5
  from importlib import import_module
4
6
  from importlib.util import find_spec
7
+ from os.path import isfile
5
8
  from pkgutil import walk_packages
9
+ from types import MappingProxyType
6
10
  from types import ModuleType as PythonModule
7
- from typing import ContextManager
11
+ from typing import ClassVar, ContextManager, Self
8
12
 
9
13
  from injection import Module, mod
10
- from injection import __name__ as injection_package_name
11
14
 
12
- __all__ = ("load_modules_with_keywords", "load_packages", "load_profile")
15
+ __all__ = ("PythonModuleLoader", "load_packages", "load_profile")
13
16
 
14
17
 
15
18
  def load_profile(*names: str) -> ContextManager[Module]:
@@ -21,76 +24,107 @@ def load_profile(*names: str) -> ContextManager[Module]:
21
24
  return mod().load_profile(*names)
22
25
 
23
26
 
24
- def load_modules_with_keywords(
27
+ def load_packages(
25
28
  *packages: PythonModule | str,
26
- keywords: Collection[str] | None = None,
29
+ predicate: Callable[[str], bool] = lambda module_name: True,
27
30
  ) -> dict[str, PythonModule]:
28
31
  """
29
- Function to import modules from a Python package if one of the keywords is contained in the Python script.
30
- The default keywords are:
31
- - `from injection `
32
- - `from injection.`
33
- - `import injection`
32
+ Function for importing all modules in a Python package.
33
+ Pass the `predicate` parameter if you want to filter the modules to be imported.
34
34
  """
35
35
 
36
- if keywords is None:
37
- keywords = (
38
- f"from {injection_package_name} ",
39
- f"from {injection_package_name}.",
40
- f"import {injection_package_name}",
36
+ return PythonModuleLoader(predicate).load(*packages).modules
37
+
38
+
39
+ @dataclass(repr=False, eq=False, frozen=True, slots=True)
40
+ class PythonModuleLoader:
41
+ predicate: Callable[[str], bool]
42
+ __modules: dict[str, PythonModule | None] = field(
43
+ default_factory=dict,
44
+ init=False,
45
+ )
46
+
47
+ # To easily mock `sys.modules` in tests
48
+ _sys_modules: ClassVar[Mapping[str, PythonModule]] = MappingProxyType(sys.modules)
49
+
50
+ @property
51
+ def modules(self) -> dict[str, PythonModule]:
52
+ return {
53
+ name: module
54
+ for name, module in self.__modules.items()
55
+ if module is not None
56
+ }
57
+
58
+ def load(self, *packages: PythonModule | str) -> Self:
59
+ modules = itertools.chain.from_iterable(
60
+ self.__iter_modules(package) for package in packages
41
61
  )
62
+ self.__modules.update(modules)
63
+ return self
42
64
 
43
- def predicate(module_name: str) -> bool:
44
- spec = find_spec(module_name)
65
+ def __is_already_loaded(self, module_name: str) -> bool:
66
+ return any(
67
+ module_name in modules for modules in (self.__modules, self._sys_modules)
68
+ )
45
69
 
46
- if spec and (module_path := spec.origin):
47
- with open(module_path, "r") as file:
48
- python_script = file.read()
70
+ def __iter_modules(
71
+ self,
72
+ package: PythonModule | str,
73
+ ) -> Iterator[tuple[str, PythonModule | None]]:
74
+ if isinstance(package, str):
75
+ package = import_module(package)
49
76
 
50
- return bool(python_script) and any(
51
- keyword in python_script for keyword in keywords
52
- )
77
+ package_name = package.__name__
53
78
 
54
- return False
79
+ try:
80
+ package_path = package.__path__
81
+ except AttributeError as exc:
82
+ raise TypeError(f"`{package_name}` isn't Python package.") from exc
55
83
 
56
- return load_packages(*packages, predicate=predicate)
84
+ for info in walk_packages(path=package_path, prefix=f"{package_name}."):
85
+ name = info.name
57
86
 
87
+ if info.ispkg or self.__is_already_loaded(name):
88
+ continue
58
89
 
59
- def load_packages(
60
- *packages: PythonModule | str,
61
- predicate: Callable[[str], bool] = lambda module_name: True,
62
- ) -> dict[str, PythonModule]:
63
- """
64
- Function for importing all modules in a Python package.
65
- Pass the `predicate` parameter if you want to filter the modules to be imported.
66
- """
90
+ module = import_module(name) if self.predicate(name) else None
91
+ yield name, module
67
92
 
68
- loaded: dict[str, PythonModule] = {}
93
+ @classmethod
94
+ def from_keywords(cls, *keywords: str) -> Self:
95
+ """
96
+ Create loader to import modules from a Python package if one of the keywords is
97
+ contained in the Python script.
98
+ """
69
99
 
70
- for package in packages:
71
- if isinstance(package, str):
72
- package = import_module(package)
100
+ def predicate(module_name: str) -> bool:
101
+ spec = find_spec(module_name)
102
+
103
+ if spec is None:
104
+ return False
73
105
 
74
- loaded |= __iter_modules_from(package, predicate)
106
+ module_path = spec.origin
75
107
 
76
- return loaded
108
+ if module_path is None or not isfile(module_path):
109
+ return False
77
110
 
111
+ with open(module_path, "r") as script:
112
+ return any(keyword in line for line in script for keyword in keywords)
78
113
 
79
- def __iter_modules_from(
80
- package: PythonModule,
81
- predicate: Callable[[str], bool],
82
- ) -> Iterator[tuple[str, PythonModule]]:
83
- package_name = package.__name__
114
+ return cls(predicate)
84
115
 
85
- try:
86
- package_path = package.__path__
87
- except AttributeError as exc:
88
- raise TypeError(f"`{package_name}` isn't Python package.") from exc
116
+ @classmethod
117
+ def startswith(cls, *prefixes: str) -> Self:
118
+ def predicate(module_name: str) -> bool:
119
+ script_name = module_name.split(".")[-1]
120
+ return any(script_name.startswith(prefix) for prefix in prefixes)
89
121
 
90
- for info in walk_packages(path=package_path, prefix=f"{package_name}."):
91
- name = info.name
122
+ return cls(predicate)
92
123
 
93
- if info.ispkg or name in sys.modules or not predicate(name):
94
- continue
124
+ @classmethod
125
+ def endswith(cls, *suffixes: str) -> Self:
126
+ def predicate(module_name: str) -> bool:
127
+ script_name = module_name.split(".")[-1]
128
+ return any(script_name.endswith(suffix) for suffix in suffixes)
95
129
 
96
- yield name, import_module(name)
130
+ return cls(predicate)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-injection
3
- Version: 0.14.6.post1
3
+ Version: 0.15.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
@@ -2,17 +2,17 @@ injection/__init__.py,sha256=a-rBAlBTiH6TzZ-11NGK6diovfibyKmmnf99PZUog1A,1166
2
2
  injection/__init__.pyi,sha256=rOl1kA9BkIeXDr7OdBMifAg9xWwvme447yjDZooir18,10398
3
3
  injection/exceptions.py,sha256=v57yMujiq6H_zwwn30A8UYEZX9R9k-bY8FnsdaimPM4,1025
4
4
  injection/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
- injection/utils.py,sha256=EuHMrix6gx2YnnUAn2_BPsDkvucGS5-pFhM3596oBK4,2796
5
+ injection/utils.py,sha256=bLIVA_3N3mTEQ3kGV4YzrWEnokHxUGqWYNKPggOOnpg,4065
6
6
  injection/_core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
7
  injection/_core/descriptors.py,sha256=7fSHlgAqmgR_Uta8KocBapOt1Xyj2dI7RY9ZdoStTzw,726
8
8
  injection/_core/injectables.py,sha256=idNkQZZ29vd73G_lE-eS5C7zGeVe_ALNkUt8M6YjZrk,5519
9
- injection/_core/module.py,sha256=DLw0pD3HDXfNhzbWM0yeCDKg-Mwg8JAzqZq43tFAXik,31814
10
- injection/_core/scope.py,sha256=fZ6zvuP9RO_9wZvMcM13_elSoztMaYKen1MgTP3s8t4,6555
9
+ injection/_core/module.py,sha256=KrEr66q1lazwz7F7jYscpr-ExX8aOwYHEho1IotXa54,31814
10
+ injection/_core/scope.py,sha256=LGT_Sk0b2FXB_ScXfLhU2BWz_bEE0vdJOV5p-ijCDq4,6552
11
11
  injection/_core/slots.py,sha256=6LoG0XtaRnIGDSG8s-FfUIw_50gL0bl4X3Fo_n-hdak,680
12
12
  injection/_core/common/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
13
13
  injection/_core/common/asynchronous.py,sha256=QeS2Lc4gEBFvTA_snOWfme5mTL4BFZWqZ8EzJwOdVos,1816
14
14
  injection/_core/common/event.py,sha256=XjzV8gxtGlGvzZs_ykvoC60qmdpd3RN08Eiqz5QUwes,1236
15
- injection/_core/common/invertible.py,sha256=YZlAdh6bNJgf1-74TRjwJTm8xrlgY95ZhOUGLSJ4XcY,482
15
+ injection/_core/common/invertible.py,sha256=gA_vw5nBjgp_w9MrDK5jMO8lhuOQWON8BbPpKzEuIY0,502
16
16
  injection/_core/common/key.py,sha256=ghkZD-Y8Moz6SEPNgMh3xgsZUjDVq-XYAmXaCu5VuCA,80
17
17
  injection/_core/common/lazy.py,sha256=6xh5h0lmaNvl32V0WoX4VCTsNJ3zUJdWVqpLJ_YeIIU,1363
18
18
  injection/_core/common/type.py,sha256=SCDtmBv9qFvEf5o5tTgCuwMDfuo1fgjSW0bUqA8ACis,2251
@@ -20,6 +20,6 @@ injection/integrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3
20
20
  injection/integrations/fastapi.py,sha256=YHSs85_3m6TUVtOwUcV157b3UZJQIw_aXWAg199a-YE,594
21
21
  injection/testing/__init__.py,sha256=SiImXDd0-DO1a8S5nbUQRtgDX8iaU_nHcp8DdqwtD2M,896
22
22
  injection/testing/__init__.pyi,sha256=iOii0i9F5n7znltGeGQYI2KXC_if9SAogLh1h03yx-0,540
23
- python_injection-0.14.6.post1.dist-info/METADATA,sha256=g-UJxN15zRFae7U8t5Fv2KU-naXLksipOjwefxrmBhs,3205
24
- python_injection-0.14.6.post1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
25
- python_injection-0.14.6.post1.dist-info/RECORD,,
23
+ python_injection-0.15.0.dist-info/METADATA,sha256=LA4sTrPDIUAk-dwOH1zKj1Rjk-feJwq1r2eYHpLIJTw,3199
24
+ python_injection-0.15.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
25
+ python_injection-0.15.0.dist-info/RECORD,,