pico-ioc 0.6.0__py3-none-any.whl → 1.0.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.
- pico_ioc/__init__.py +14 -10
- pico_ioc/_state.py +2 -0
- pico_ioc/_version.py +1 -1
- pico_ioc/api.py +91 -37
- pico_ioc/container.py +133 -21
- pico_ioc/decorators.py +48 -5
- pico_ioc/public_api.py +76 -0
- pico_ioc/resolver.py +101 -49
- pico_ioc/scanner.py +181 -48
- pico_ioc/typing_utils.py +9 -4
- pico_ioc-1.0.0.dist-info/METADATA +145 -0
- pico_ioc-1.0.0.dist-info/RECORD +17 -0
- pico_ioc-0.6.0.dist-info/METADATA +0 -290
- pico_ioc-0.6.0.dist-info/RECORD +0 -16
- {pico_ioc-0.6.0.dist-info → pico_ioc-1.0.0.dist-info}/WHEEL +0 -0
- {pico_ioc-0.6.0.dist-info → pico_ioc-1.0.0.dist-info}/licenses/LICENSE +0 -0
- {pico_ioc-0.6.0.dist-info → pico_ioc-1.0.0.dist-info}/top_level.txt +0 -0
pico_ioc/__init__.py
CHANGED
|
@@ -1,27 +1,31 @@
|
|
|
1
|
-
|
|
2
|
-
from .container import PicoContainer, Binder
|
|
3
|
-
from .decorators import component, factory_component, provides
|
|
4
|
-
from .plugins import PicoPlugin
|
|
5
|
-
from .resolver import Resolver
|
|
6
|
-
from .api import init, reset
|
|
7
|
-
from .proxy import ComponentProxy
|
|
1
|
+
# pico_ioc/__init__.py
|
|
8
2
|
|
|
9
3
|
try:
|
|
10
4
|
from ._version import __version__
|
|
11
5
|
except Exception:
|
|
12
6
|
__version__ = "0.0.0"
|
|
13
7
|
|
|
8
|
+
from .container import PicoContainer, Binder
|
|
9
|
+
from .decorators import component, factory_component, provides, plugin, Qualifier, qualifier
|
|
10
|
+
from .plugins import PicoPlugin
|
|
11
|
+
from .resolver import Resolver
|
|
12
|
+
from .api import init, reset
|
|
13
|
+
from .proxy import ComponentProxy
|
|
14
|
+
|
|
14
15
|
__all__ = [
|
|
15
16
|
"__version__",
|
|
16
17
|
"PicoContainer",
|
|
17
18
|
"Binder",
|
|
18
19
|
"PicoPlugin",
|
|
20
|
+
"ComponentProxy",
|
|
19
21
|
"init",
|
|
20
|
-
"reset",
|
|
22
|
+
"reset",
|
|
21
23
|
"component",
|
|
22
24
|
"factory_component",
|
|
23
25
|
"provides",
|
|
24
|
-
"
|
|
25
|
-
"
|
|
26
|
+
"plugin",
|
|
27
|
+
"Qualifier",
|
|
28
|
+
"qualifier",
|
|
29
|
+
"Resolver",
|
|
26
30
|
]
|
|
27
31
|
|
pico_ioc/_state.py
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
# pico_ioc/_state.py
|
|
2
2
|
from contextvars import ContextVar
|
|
3
|
+
from typing import Optional
|
|
3
4
|
|
|
4
5
|
_scanning: ContextVar[bool] = ContextVar("pico_scanning", default=False)
|
|
5
6
|
_resolving: ContextVar[bool] = ContextVar("pico_resolving", default=False)
|
|
6
7
|
|
|
7
8
|
_container = None
|
|
9
|
+
_root_name: Optional[str] = None
|
|
8
10
|
|
pico_ioc/_version.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = '0.
|
|
1
|
+
__version__ = '1.0.0'
|
pico_ioc/api.py
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
|
+
# pico_ioc/api.py
|
|
2
|
+
|
|
1
3
|
import inspect
|
|
2
4
|
import logging
|
|
5
|
+
from contextlib import contextmanager
|
|
3
6
|
from typing import Callable, Optional, Tuple
|
|
7
|
+
|
|
4
8
|
from .container import PicoContainer, Binder
|
|
5
9
|
from .plugins import PicoPlugin
|
|
6
10
|
from .scanner import scan_and_configure
|
|
@@ -8,7 +12,9 @@ from . import _state
|
|
|
8
12
|
|
|
9
13
|
|
|
10
14
|
def reset() -> None:
|
|
15
|
+
"""Reset the global container."""
|
|
11
16
|
_state._container = None
|
|
17
|
+
_state._root_name = None
|
|
12
18
|
|
|
13
19
|
|
|
14
20
|
def init(
|
|
@@ -19,56 +25,104 @@ def init(
|
|
|
19
25
|
plugins: Tuple[PicoPlugin, ...] = (),
|
|
20
26
|
reuse: bool = True,
|
|
21
27
|
) -> PicoContainer:
|
|
22
|
-
|
|
28
|
+
"""
|
|
29
|
+
Initialize and configure a PicoContainer by scanning a root package.
|
|
30
|
+
"""
|
|
31
|
+
root_name = root_package if isinstance(root_package, str) else getattr(root_package, "__name__", None)
|
|
32
|
+
|
|
33
|
+
# Reuse only if the existing container was built for the same root
|
|
34
|
+
if reuse and _state._container and _state._root_name == root_name:
|
|
23
35
|
return _state._container
|
|
24
36
|
|
|
25
|
-
combined_exclude = exclude
|
|
26
|
-
if auto_exclude_caller:
|
|
27
|
-
try:
|
|
28
|
-
caller_frame = inspect.stack()[1].frame
|
|
29
|
-
caller_module = inspect.getmodule(caller_frame)
|
|
30
|
-
caller_name = getattr(caller_module, "__name__", None)
|
|
31
|
-
except Exception:
|
|
32
|
-
caller_name = None
|
|
33
|
-
if caller_name:
|
|
34
|
-
if combined_exclude is None:
|
|
35
|
-
def combined_exclude(mod: str, _caller=caller_name):
|
|
36
|
-
return mod == _caller
|
|
37
|
-
else:
|
|
38
|
-
prev = combined_exclude
|
|
39
|
-
def combined_exclude(mod: str, _caller=caller_name, _prev=prev):
|
|
40
|
-
return mod == _caller or _prev(mod)
|
|
37
|
+
combined_exclude = _build_exclude(exclude, auto_exclude_caller, root_name=root_name)
|
|
41
38
|
|
|
42
39
|
container = PicoContainer()
|
|
43
40
|
binder = Binder(container)
|
|
44
41
|
logging.info("Initializing pico-ioc...")
|
|
45
42
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
43
|
+
with _scanning_flag():
|
|
44
|
+
scan_and_configure(
|
|
45
|
+
root_package,
|
|
46
|
+
container,
|
|
47
|
+
exclude=combined_exclude,
|
|
48
|
+
plugins=plugins,
|
|
49
|
+
)
|
|
51
50
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
getattr(pl, "after_bind", lambda *a, **k: None)(container, binder)
|
|
55
|
-
except Exception:
|
|
56
|
-
logging.exception("Plugin after_bind failed")
|
|
57
|
-
for pl in plugins:
|
|
58
|
-
try:
|
|
59
|
-
getattr(pl, "before_eager", lambda *a, **k: None)(container, binder)
|
|
60
|
-
except Exception:
|
|
61
|
-
logging.exception("Plugin before_eager failed")
|
|
51
|
+
_run_hooks(plugins, "after_bind", container, binder)
|
|
52
|
+
_run_hooks(plugins, "before_eager", container, binder)
|
|
62
53
|
|
|
63
54
|
container.eager_instantiate_all()
|
|
64
55
|
|
|
65
|
-
|
|
66
|
-
try:
|
|
67
|
-
getattr(pl, "after_ready", lambda *a, **k: None)(container, binder)
|
|
68
|
-
except Exception:
|
|
69
|
-
logging.exception("Plugin after_ready failed")
|
|
56
|
+
_run_hooks(plugins, "after_ready", container, binder)
|
|
70
57
|
|
|
71
58
|
logging.info("Container configured and ready.")
|
|
72
59
|
_state._container = container
|
|
60
|
+
_state._root_name = root_name # remember which root this container represents
|
|
73
61
|
return container
|
|
74
62
|
|
|
63
|
+
|
|
64
|
+
# -------------------- helpers --------------------
|
|
65
|
+
|
|
66
|
+
def _build_exclude(
|
|
67
|
+
exclude: Optional[Callable[[str], bool]],
|
|
68
|
+
auto_exclude_caller: bool,
|
|
69
|
+
*,
|
|
70
|
+
root_name: Optional[str] = None,
|
|
71
|
+
) -> Optional[Callable[[str], bool]]:
|
|
72
|
+
"""
|
|
73
|
+
Compose the exclude predicate. When auto_exclude_caller=True, exclude only
|
|
74
|
+
the exact calling module, but never exclude modules under the root being scanned.
|
|
75
|
+
"""
|
|
76
|
+
if not auto_exclude_caller:
|
|
77
|
+
return exclude
|
|
78
|
+
|
|
79
|
+
caller = _get_caller_module_name()
|
|
80
|
+
if not caller:
|
|
81
|
+
return exclude
|
|
82
|
+
|
|
83
|
+
def _under_root(mod: str) -> bool:
|
|
84
|
+
return bool(root_name) and (mod == root_name or mod.startswith(root_name + "."))
|
|
85
|
+
|
|
86
|
+
if exclude is None:
|
|
87
|
+
return lambda mod, _caller=caller: (mod == _caller) and not _under_root(mod)
|
|
88
|
+
|
|
89
|
+
prev = exclude
|
|
90
|
+
return lambda mod, _caller=caller, _prev=prev: (((mod == _caller) and not _under_root(mod)) or _prev(mod))
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _get_caller_module_name() -> Optional[str]:
|
|
94
|
+
"""Return the module name that called `init`."""
|
|
95
|
+
try:
|
|
96
|
+
f = inspect.currentframe()
|
|
97
|
+
# frame -> _get_caller_module_name -> _build_exclude -> init
|
|
98
|
+
if f and f.f_back and f.f_back.f_back and f.f_back.f_back.f_back:
|
|
99
|
+
mod = inspect.getmodule(f.f_back.f_back.f_back)
|
|
100
|
+
return getattr(mod, "__name__", None)
|
|
101
|
+
except Exception:
|
|
102
|
+
pass
|
|
103
|
+
return None
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _run_hooks(
|
|
107
|
+
plugins: Tuple[PicoPlugin, ...],
|
|
108
|
+
hook_name: str,
|
|
109
|
+
container: PicoContainer,
|
|
110
|
+
binder: Binder,
|
|
111
|
+
) -> None:
|
|
112
|
+
for pl in plugins:
|
|
113
|
+
try:
|
|
114
|
+
fn = getattr(pl, hook_name, None)
|
|
115
|
+
if fn:
|
|
116
|
+
fn(container, binder)
|
|
117
|
+
except Exception:
|
|
118
|
+
logging.exception("Plugin %s failed", hook_name)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
@contextmanager
|
|
122
|
+
def _scanning_flag():
|
|
123
|
+
tok = _state._scanning.set(True)
|
|
124
|
+
try:
|
|
125
|
+
yield
|
|
126
|
+
finally:
|
|
127
|
+
_state._scanning.reset(tok)
|
|
128
|
+
|
pico_ioc/container.py
CHANGED
|
@@ -1,43 +1,155 @@
|
|
|
1
1
|
# pico_ioc/container.py
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
import inspect
|
|
4
|
+
from typing import Any, Dict, get_origin, get_args, Annotated
|
|
5
|
+
import typing as _t
|
|
6
|
+
|
|
7
|
+
from .decorators import QUALIFIERS_KEY
|
|
8
|
+
from . import _state # re-entrancy guard
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Binder:
|
|
12
|
+
def __init__(self, container: "PicoContainer"):
|
|
13
|
+
self._c = container
|
|
14
|
+
|
|
15
|
+
def bind(self, key: Any, provider, *, lazy: bool):
|
|
16
|
+
self._c.bind(key, provider, lazy=lazy)
|
|
17
|
+
|
|
18
|
+
def has(self, key: Any) -> bool:
|
|
19
|
+
return self._c.has(key)
|
|
20
|
+
|
|
21
|
+
def get(self, key: Any):
|
|
22
|
+
return self._c.get(key)
|
|
2
23
|
|
|
3
|
-
from typing import Any, Callable, Dict
|
|
4
|
-
from ._state import _scanning, _resolving
|
|
5
24
|
|
|
6
25
|
class PicoContainer:
|
|
7
26
|
def __init__(self):
|
|
8
27
|
self._providers: Dict[Any, Dict[str, Any]] = {}
|
|
9
28
|
self._singletons: Dict[Any, Any] = {}
|
|
10
29
|
|
|
11
|
-
def bind(self, key: Any, provider
|
|
12
|
-
|
|
30
|
+
def bind(self, key: Any, provider, *, lazy: bool):
|
|
31
|
+
meta = {"factory": provider, "lazy": bool(lazy)}
|
|
32
|
+
try:
|
|
33
|
+
q = getattr(key, QUALIFIERS_KEY, ())
|
|
34
|
+
except Exception:
|
|
35
|
+
q = ()
|
|
36
|
+
meta["qualifiers"] = tuple(q) if q else ()
|
|
37
|
+
self._providers[key] = meta
|
|
13
38
|
|
|
14
39
|
def has(self, key: Any) -> bool:
|
|
15
|
-
return key in self._providers
|
|
40
|
+
return key in self._providers
|
|
16
41
|
|
|
42
|
+
def get(self, key: Any):
|
|
43
|
+
# block only when scanning and NOT currently resolving a dependency
|
|
44
|
+
if _state._scanning.get() and not _state._resolving.get():
|
|
45
|
+
raise RuntimeError("re-entrant container access during scan")
|
|
17
46
|
|
|
18
|
-
|
|
19
|
-
if
|
|
20
|
-
raise
|
|
47
|
+
prov = self._providers.get(key)
|
|
48
|
+
if prov is None:
|
|
49
|
+
raise NameError(f"No provider found for key {key!r}")
|
|
21
50
|
|
|
22
51
|
if key in self._singletons:
|
|
23
52
|
return self._singletons[key]
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
53
|
+
|
|
54
|
+
# mark resolving around factory execution
|
|
55
|
+
tok = _state._resolving.set(True)
|
|
56
|
+
try:
|
|
57
|
+
instance = prov["factory"]()
|
|
58
|
+
finally:
|
|
59
|
+
_state._resolving.reset(tok)
|
|
60
|
+
|
|
61
|
+
# memoize always (both lazy and non-lazy after first get)
|
|
28
62
|
self._singletons[key] = instance
|
|
29
63
|
return instance
|
|
30
64
|
|
|
31
|
-
|
|
32
65
|
def eager_instantiate_all(self):
|
|
33
|
-
for key,
|
|
34
|
-
if not
|
|
66
|
+
for key, prov in list(self._providers.items()):
|
|
67
|
+
if not prov["lazy"]:
|
|
35
68
|
self.get(key)
|
|
36
69
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
def
|
|
41
|
-
|
|
42
|
-
|
|
70
|
+
def get_all(self, base_type: Any):
|
|
71
|
+
return tuple(self._resolve_all_for_base(base_type, qualifiers=()))
|
|
72
|
+
|
|
73
|
+
def get_all_qualified(self, base_type: Any, *qualifiers: str):
|
|
74
|
+
return tuple(self._resolve_all_for_base(base_type, qualifiers=qualifiers))
|
|
75
|
+
|
|
76
|
+
def _resolve_all_for_base(self, base_type: Any, qualifiers=()):
|
|
77
|
+
matches = []
|
|
78
|
+
for provider_key, meta in self._providers.items():
|
|
79
|
+
cls = provider_key if isinstance(provider_key, type) else None
|
|
80
|
+
if cls is None:
|
|
81
|
+
continue
|
|
82
|
+
|
|
83
|
+
# Avoid self-inclusion loops: if the class itself requires a collection
|
|
84
|
+
# of `base_type` in its __init__, don't treat it as an implementation
|
|
85
|
+
# of `base_type` when building that collection.
|
|
86
|
+
if _requires_collection_of_base(cls, base_type):
|
|
87
|
+
continue
|
|
88
|
+
|
|
89
|
+
if _is_compatible(cls, base_type):
|
|
90
|
+
prov_qs = meta.get("qualifiers", ())
|
|
91
|
+
if all(q in prov_qs for q in qualifiers):
|
|
92
|
+
inst = self.get(provider_key)
|
|
93
|
+
matches.append(inst)
|
|
94
|
+
return matches
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _is_protocol(t) -> bool:
|
|
98
|
+
return getattr(t, "_is_protocol", False) is True
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _is_compatible(cls, base) -> bool:
|
|
102
|
+
try:
|
|
103
|
+
if isinstance(base, type) and issubclass(cls, base):
|
|
104
|
+
return True
|
|
105
|
+
except TypeError:
|
|
106
|
+
pass
|
|
107
|
+
|
|
108
|
+
if _is_protocol(base):
|
|
109
|
+
# simple structural check: ensure methods/attrs declared on the Protocol exist on the class
|
|
110
|
+
names = set(getattr(base, "__annotations__", {}).keys())
|
|
111
|
+
names.update(n for n in getattr(base, "__dict__", {}).keys() if not n.startswith("_"))
|
|
112
|
+
for n in names:
|
|
113
|
+
if n.startswith("__") and n.endswith("__"):
|
|
114
|
+
continue
|
|
115
|
+
if not hasattr(cls, n):
|
|
116
|
+
return False
|
|
117
|
+
return True
|
|
118
|
+
|
|
119
|
+
return False
|
|
120
|
+
|
|
121
|
+
def _requires_collection_of_base(cls, base) -> bool:
|
|
122
|
+
"""
|
|
123
|
+
Return True if `cls.__init__` has any parameter annotated as a collection
|
|
124
|
+
(list/tuple, including Annotated variants) of `base`. This prevents treating
|
|
125
|
+
`cls` as an implementation of `base` while building that collection,
|
|
126
|
+
avoiding recursion.
|
|
127
|
+
"""
|
|
128
|
+
try:
|
|
129
|
+
sig = inspect.signature(cls.__init__)
|
|
130
|
+
except Exception:
|
|
131
|
+
return False
|
|
132
|
+
|
|
133
|
+
try:
|
|
134
|
+
from .resolver import _get_hints # type: ignore
|
|
135
|
+
hints = _get_hints(cls.__init__, owner_cls=cls)
|
|
136
|
+
except Exception:
|
|
137
|
+
hints = {}
|
|
138
|
+
|
|
139
|
+
for name, param in sig.parameters.items():
|
|
140
|
+
if name == "self":
|
|
141
|
+
continue
|
|
142
|
+
ann = hints.get(name, param.annotation)
|
|
143
|
+
origin = get_origin(ann) or ann
|
|
144
|
+
if origin in (list, tuple, _t.List, _t.Tuple):
|
|
145
|
+
inner = (get_args(ann) or (object,))[0]
|
|
146
|
+
# Unwrap Annotated[T, ...] si aparece
|
|
147
|
+
if get_origin(inner) is Annotated:
|
|
148
|
+
args = get_args(inner)
|
|
149
|
+
if args:
|
|
150
|
+
inner = args[0]
|
|
151
|
+
if inner is base:
|
|
152
|
+
return True
|
|
153
|
+
return False
|
|
154
|
+
|
|
43
155
|
|
pico_ioc/decorators.py
CHANGED
|
@@ -1,28 +1,36 @@
|
|
|
1
1
|
# pico_ioc/decorators.py
|
|
2
|
-
|
|
2
|
+
from __future__ import annotations
|
|
3
3
|
import functools
|
|
4
|
-
from typing import Any
|
|
4
|
+
from typing import Any, Iterable
|
|
5
5
|
|
|
6
6
|
COMPONENT_FLAG = "_is_component"
|
|
7
7
|
COMPONENT_KEY = "_component_key"
|
|
8
8
|
COMPONENT_LAZY = "_component_lazy"
|
|
9
|
+
|
|
9
10
|
FACTORY_FLAG = "_is_factory_component"
|
|
10
11
|
PROVIDES_KEY = "_provides_name"
|
|
11
12
|
PROVIDES_LAZY = "_pico_lazy"
|
|
12
13
|
|
|
14
|
+
PLUGIN_FLAG = "_is_pico_plugin"
|
|
15
|
+
QUALIFIERS_KEY = "_pico_qualifiers"
|
|
16
|
+
|
|
17
|
+
|
|
13
18
|
def factory_component(cls):
|
|
14
19
|
setattr(cls, FACTORY_FLAG, True)
|
|
15
20
|
return cls
|
|
16
21
|
|
|
22
|
+
|
|
17
23
|
def provides(key: Any, *, lazy: bool = False):
|
|
18
|
-
def dec(
|
|
19
|
-
@functools.wraps(
|
|
20
|
-
def w(*a, **k):
|
|
24
|
+
def dec(fn):
|
|
25
|
+
@functools.wraps(fn)
|
|
26
|
+
def w(*a, **k):
|
|
27
|
+
return fn(*a, **k)
|
|
21
28
|
setattr(w, PROVIDES_KEY, key)
|
|
22
29
|
setattr(w, PROVIDES_LAZY, bool(lazy))
|
|
23
30
|
return w
|
|
24
31
|
return dec
|
|
25
32
|
|
|
33
|
+
|
|
26
34
|
def component(cls=None, *, name: Any = None, lazy: bool = False):
|
|
27
35
|
def dec(c):
|
|
28
36
|
setattr(c, COMPONENT_FLAG, True)
|
|
@@ -31,3 +39,38 @@ def component(cls=None, *, name: Any = None, lazy: bool = False):
|
|
|
31
39
|
return c
|
|
32
40
|
return dec(cls) if cls else dec
|
|
33
41
|
|
|
42
|
+
|
|
43
|
+
def plugin(cls):
|
|
44
|
+
setattr(cls, PLUGIN_FLAG, True)
|
|
45
|
+
return cls
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class Qualifier(str):
|
|
49
|
+
__slots__ = () # tiny memory win; immutable like str
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def qualifier(*qs: Qualifier):
|
|
53
|
+
def dec(cls):
|
|
54
|
+
current: Iterable[Qualifier] = getattr(cls, QUALIFIERS_KEY, ())
|
|
55
|
+
seen = set(current)
|
|
56
|
+
merged = list(current)
|
|
57
|
+
for q in qs:
|
|
58
|
+
if q not in seen:
|
|
59
|
+
merged.append(q)
|
|
60
|
+
seen.add(q)
|
|
61
|
+
setattr(cls, QUALIFIERS_KEY, tuple(merged))
|
|
62
|
+
return cls
|
|
63
|
+
return dec
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
__all__ = [
|
|
67
|
+
# decorators
|
|
68
|
+
"component", "factory_component", "provides", "plugin", "qualifier",
|
|
69
|
+
# qualifier type
|
|
70
|
+
"Qualifier",
|
|
71
|
+
# metadata keys (exported for advanced use/testing)
|
|
72
|
+
"COMPONENT_FLAG", "COMPONENT_KEY", "COMPONENT_LAZY",
|
|
73
|
+
"FACTORY_FLAG", "PROVIDES_KEY", "PROVIDES_LAZY",
|
|
74
|
+
"PLUGIN_FLAG", "QUALIFIERS_KEY",
|
|
75
|
+
]
|
|
76
|
+
|
pico_ioc/public_api.py
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# pico_ioc/public_api.py
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
import importlib
|
|
5
|
+
import inspect
|
|
6
|
+
import pkgutil
|
|
7
|
+
import sys
|
|
8
|
+
from types import ModuleType
|
|
9
|
+
from typing import Dict, Iterable, Optional, Tuple
|
|
10
|
+
|
|
11
|
+
from .decorators import COMPONENT_FLAG, FACTORY_FLAG, PLUGIN_FLAG
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def export_public_symbols_decorated(
|
|
15
|
+
*packages: str,
|
|
16
|
+
include_also: Optional[Iterable[str]] = None,
|
|
17
|
+
include_plugins: bool = True,
|
|
18
|
+
):
|
|
19
|
+
index: Dict[str, Tuple[str, str]] = {}
|
|
20
|
+
|
|
21
|
+
def _collect(m: ModuleType):
|
|
22
|
+
names = getattr(m, "__all__", None)
|
|
23
|
+
if isinstance(names, (list, tuple, set)):
|
|
24
|
+
for n in names:
|
|
25
|
+
if hasattr(m, n):
|
|
26
|
+
index.setdefault(n, (m.__name__, n))
|
|
27
|
+
return
|
|
28
|
+
|
|
29
|
+
for n, obj in m.__dict__.items():
|
|
30
|
+
if not inspect.isclass(obj):
|
|
31
|
+
continue
|
|
32
|
+
is_component = getattr(obj, COMPONENT_FLAG, False)
|
|
33
|
+
is_factory = getattr(obj, FACTORY_FLAG, False)
|
|
34
|
+
is_plugin = include_plugins and getattr(obj, PLUGIN_FLAG, False)
|
|
35
|
+
if is_component or is_factory or is_plugin:
|
|
36
|
+
index.setdefault(n, (m.__name__, n))
|
|
37
|
+
|
|
38
|
+
for pkg_name in packages:
|
|
39
|
+
try:
|
|
40
|
+
base = importlib.import_module(pkg_name)
|
|
41
|
+
except Exception:
|
|
42
|
+
continue
|
|
43
|
+
if hasattr(base, "__path__"):
|
|
44
|
+
prefix = base.__name__ + "."
|
|
45
|
+
for _, modname, _ in pkgutil.walk_packages(base.__path__, prefix):
|
|
46
|
+
try:
|
|
47
|
+
m = importlib.import_module(modname)
|
|
48
|
+
except Exception:
|
|
49
|
+
continue
|
|
50
|
+
_collect(m)
|
|
51
|
+
else:
|
|
52
|
+
_collect(base)
|
|
53
|
+
|
|
54
|
+
for qual in tuple(include_also or ()):
|
|
55
|
+
modname, _, attr = qual.partition(":")
|
|
56
|
+
if modname and attr:
|
|
57
|
+
try:
|
|
58
|
+
m = importlib.import_module(modname)
|
|
59
|
+
if hasattr(m, attr):
|
|
60
|
+
index.setdefault(attr, (m.__name__, attr))
|
|
61
|
+
except Exception:
|
|
62
|
+
pass
|
|
63
|
+
|
|
64
|
+
def __getattr__(name: str):
|
|
65
|
+
try:
|
|
66
|
+
modname, attr = index[name]
|
|
67
|
+
except KeyError as e:
|
|
68
|
+
raise AttributeError(f"module has no attribute {name!r}") from e
|
|
69
|
+
mod = sys.modules.get(modname) or importlib.import_module(modname)
|
|
70
|
+
return getattr(mod, attr)
|
|
71
|
+
|
|
72
|
+
def __dir__():
|
|
73
|
+
return sorted(index.keys())
|
|
74
|
+
|
|
75
|
+
return __getattr__, __dir__
|
|
76
|
+
|