pico-ioc 1.5.0__py3-none-any.whl → 2.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/builder.py DELETED
@@ -1,210 +0,0 @@
1
- from __future__ import annotations
2
- import inspect as _inspect
3
- import logging
4
- import os
5
- from typing import Any, Callable, Dict, Iterable, List, Optional, Sequence, Tuple
6
- from typing import get_origin, get_args, Annotated
7
-
8
- from .interceptors import MethodInterceptor, ContainerInterceptor
9
- from .container import PicoContainer, _is_compatible
10
- from .policy import apply_policy, _conditional_active
11
- from .plugins import PicoPlugin, run_plugin_hook
12
- from .scanner import scan_and_configure
13
- from .resolver import Resolver, _get_hints
14
- from . import _state
15
- from .config import ConfigRegistry
16
-
17
- class PicoContainerBuilder:
18
- def __init__(self):
19
- self._scan_plan: List[Tuple[Any, Optional[Callable[[str], bool]], Tuple[PicoPlugin, ...]]] = []
20
- self._overrides: Dict[Any, Any] = {}
21
- self._profiles: Optional[List[str]] = None
22
- self._plugins: Tuple[PicoPlugin, ...] = ()
23
- self._include_tags: Optional[set[str]] = None
24
- self._exclude_tags: Optional[set[str]] = None
25
- self._roots: Iterable[type] = ()
26
- self._providers: Dict[Any, Dict] = {}
27
- self._eager: bool = True
28
- self._config_registry: ConfigRegistry | None = None
29
-
30
- def with_config(self, registry: ConfigRegistry) -> "PicoContainerBuilder":
31
- self._config_registry = registry
32
- return self
33
-
34
- def with_plugins(self, plugins: Tuple[PicoPlugin, ...]) -> "PicoContainerBuilder":
35
- self._plugins = plugins or ()
36
- return self
37
-
38
- def with_profiles(self, profiles: Optional[List[str]]) -> "PicoContainerBuilder":
39
- self._profiles = profiles
40
- return self
41
-
42
- def add_scan_package(self, package: Any, exclude: Optional[Callable[[str], bool]] = None) -> "PicoContainerBuilder":
43
- self._scan_plan.append((package, exclude, self._plugins))
44
- return self
45
-
46
- def with_overrides(self, overrides: Optional[Dict[Any, Any]]) -> "PicoContainerBuilder":
47
- self._overrides = overrides or {}
48
- return self
49
-
50
- def with_tag_filters(self, include: Optional[set[str]], exclude: Optional[set[str]]) -> "PicoContainerBuilder":
51
- self._include_tags = include
52
- self._exclude_tags = exclude
53
- return self
54
-
55
- def with_roots(self, roots: Iterable[type]) -> "PicoContainerBuilder":
56
- self._roots = roots or ()
57
- return self
58
-
59
- def with_eager(self, eager: bool) -> "PicoContainerBuilder":
60
- self._eager = bool(eager)
61
- return self
62
-
63
- def build(self) -> PicoContainer:
64
- requested_profiles = _resolve_profiles(self._profiles)
65
- container = PicoContainer(providers=self._providers)
66
- container._active_profiles = tuple(requested_profiles)
67
- setattr(container, "_config_registry", self._config_registry)
68
- all_infras: list[tuple[type, dict]] = []
69
- for pkg, exclude, scan_plugins in self._scan_plan:
70
- with _state.scanning_flag():
71
- c, f, infra_decls = scan_and_configure(pkg, container, exclude=exclude, plugins=scan_plugins)
72
- logging.info("Scanned '%s' (components: %d, factories: %d)", getattr(pkg, "__name__", pkg), c, f)
73
- all_infras.extend(infra_decls)
74
- _run_infrastructure(container=container, infra_decls=all_infras, profiles=requested_profiles)
75
- binder = container.binder()
76
- if self._overrides:
77
- _apply_overrides(container, self._overrides)
78
- run_plugin_hook(self._plugins, "after_bind", container, binder)
79
- run_plugin_hook(self._plugins, "before_eager", container, binder)
80
- apply_policy(container, profiles=requested_profiles)
81
- _filter_by_tags(container, self._include_tags, self._exclude_tags)
82
- if self._roots:
83
- _restrict_to_subgraph(container, self._roots, self._overrides)
84
- run_plugin_hook(self._plugins, "after_ready", container, binder)
85
- if self._eager:
86
- container.eager_instantiate_all()
87
- logging.info("Container configured and ready.")
88
- return container
89
-
90
- def _resolve_profiles(profiles: Optional[List[str]]) -> List[str]:
91
- if profiles is not None:
92
- return list(profiles)
93
- env_val = os.getenv("PICO_PROFILE", "")
94
- return [p.strip() for p in env_val.split(",") if p.strip()]
95
-
96
- def _as_provider(val):
97
- if isinstance(val, tuple) and len(val) == 2 and callable(val[0]) and isinstance(val[1], bool):
98
- return val[0], val[1]
99
- if callable(val):
100
- return val, False
101
- return (lambda v=val: v), False
102
-
103
- def _apply_overrides(container: PicoContainer, overrides: Dict[Any, Any]) -> None:
104
- for key, val in overrides.items():
105
- provider, lazy = _as_provider(val)
106
- container.bind(key, provider, lazy=lazy)
107
-
108
- def _filter_by_tags(container: PicoContainer, include_tags: Optional[set[str]], exclude_tags: Optional[set[str]]) -> None:
109
- if not include_tags and not exclude_tags:
110
- return
111
- def _tag_ok(meta: dict) -> bool:
112
- tags = set(meta.get("tags", ()))
113
- if include_tags and not tags.intersection(include_tags):
114
- return False
115
- if exclude_tags and tags.intersection(exclude_tags):
116
- return False
117
- return True
118
- container._providers = {k: v for k, v in container._providers.items() if _tag_ok(v)}
119
-
120
- def _compute_allowed_subgraph(container: PicoContainer, roots: Iterable[type]) -> set:
121
- allowed: set[Any] = set(roots)
122
- stack = list(roots or ())
123
- def _add_impls_for_base(base_t):
124
- for prov_key, meta in container._providers.items():
125
- cls = prov_key if isinstance(prov_key, type) else None
126
- if cls is not None and _is_compatible(cls, base_t):
127
- if prov_key not in allowed:
128
- allowed.add(prov_key)
129
- stack.append(prov_key)
130
- while stack:
131
- k = stack.pop()
132
- allowed.add(k)
133
- if isinstance(k, type):
134
- _add_impls_for_base(k)
135
- cls = k if isinstance(k, type) else None
136
- if cls is None or not container.has(k):
137
- continue
138
- try:
139
- sig = _inspect.signature(cls.__init__)
140
- hints = _get_hints(cls.__init__, owner_cls=cls)
141
- except Exception:
142
- continue
143
- for pname, param in sig.parameters.items():
144
- if pname == "self":
145
- continue
146
- ann = hints.get(pname, param.annotation)
147
- origin = get_origin(ann) or ann
148
- if origin in (list, tuple):
149
- inner = (get_args(ann) or (object,))[0]
150
- if get_origin(inner) is Annotated:
151
- inner = (get_args(inner) or (object,))[0]
152
- if isinstance(inner, type):
153
- if inner not in allowed:
154
- stack.append(inner)
155
- continue
156
- if isinstance(ann, type) and ann not in allowed:
157
- stack.append(ann)
158
- elif container.has(pname) and pname not in allowed:
159
- stack.append(pname)
160
- return allowed
161
-
162
- def _restrict_to_subgraph(container: PicoContainer, roots: Iterable[type], overrides: Optional[Dict[Any, Any]]) -> None:
163
- allowed = _compute_allowed_subgraph(container, roots)
164
- keep_keys: set[Any] = allowed | (set(overrides.keys()) if overrides else set())
165
- container._providers = {k: v for k, v in container._providers.items() if k in keep_keys}
166
-
167
- def _run_infrastructure(*, container: PicoContainer, infra_decls: List[tuple[type, dict]], profiles: List[str]) -> None:
168
- def _active(meta: dict) -> bool:
169
- profs = tuple(meta.get("profiles", ())) or ()
170
- if profs and (not profiles or not any(p in profs for p in profiles)):
171
- return False
172
- req_env = tuple(meta.get("require_env", ())) or ()
173
- if req_env:
174
- import os
175
- if not all(os.getenv(k) not in (None, "") for k in req_env):
176
- return False
177
- pred = meta.get("predicate", None)
178
- if callable(pred):
179
- try:
180
- if not bool(pred()):
181
- return False
182
- except Exception:
183
- return False
184
- return True
185
- from .resolver import Resolver
186
- from .infra import Infra
187
- resolver = Resolver(container)
188
- active_infras: List[tuple[int, type]] = []
189
- for cls, meta in infra_decls:
190
- if not _active(meta):
191
- continue
192
- order = int(meta.get("order", 0))
193
- active_infras.append((order, cls))
194
- active_infras.sort(key=lambda t: (t[0], getattr(t[1], "__qualname__", "")))
195
- for _ord, cls in active_infras:
196
- try:
197
- inst = resolver.create_instance(cls)
198
- except Exception:
199
- import logging
200
- logging.exception("Failed to construct infrastructure %r", cls)
201
- continue
202
- infra = Infra(container=container, profiles=tuple(profiles))
203
- fn = getattr(inst, "configure", None)
204
- if callable(fn):
205
- try:
206
- fn(infra)
207
- except Exception:
208
- import logging
209
- logging.exception("Infrastructure configure() failed for %r", cls)
210
-
pico_ioc/config.py DELETED
@@ -1,332 +0,0 @@
1
- # src/pico_ioc/config.py
2
- from __future__ import annotations
3
-
4
- import os, json, configparser, pathlib
5
- from dataclasses import is_dataclass, fields, MISSING
6
- from typing import Any, Callable, Dict, Iterable, Optional, Sequence, Tuple, Protocol
7
-
8
- # ---- Flags & metadata on classes / fields ----
9
- _CONFIG_FLAG = "_pico_is_config_component"
10
- _CONFIG_PREFIX = "_pico_config_prefix"
11
- _FIELD_META = "_pico_config_field_meta" # dict: name -> FieldSpec
12
-
13
- # ---- Source protocol & implementations ----
14
-
15
- class ConfigSource(Protocol):
16
- def get(self, key: str) -> Optional[str]: ...
17
- def keys(self) -> Iterable[str]: ...
18
-
19
- class EnvSource:
20
- def __init__(self, prefix: str = ""):
21
- self.prefix = prefix or ""
22
- def get(self, key: str) -> Optional[str]:
23
- # try PREFIX+KEY first, then KEY
24
- v = os.getenv(self.prefix + key)
25
- if v is not None:
26
- return v
27
- return os.getenv(key)
28
- def keys(self) -> Iterable[str]:
29
- # best-effort; env keys only (without prefix expansion)
30
- return os.environ.keys()
31
-
32
- class FileSource:
33
- def __init__(self, path: os.PathLike[str] | str, optional: bool = False):
34
- self.path = str(path)
35
- self.optional = bool(optional)
36
- self._cache: Dict[str, Any] = {}
37
- self._load_once()
38
-
39
- def _load_once(self):
40
- p = pathlib.Path(self.path)
41
- if not p.exists():
42
- if self.optional:
43
- self._cache = {}
44
- return
45
- raise FileNotFoundError(self.path)
46
- text = p.read_text(encoding="utf-8")
47
-
48
- # Try in order: JSON, INI, dotenv, YAML (if available)
49
- # JSON
50
- try:
51
- data = json.loads(text)
52
- self._cache = _flatten_obj(data)
53
- return
54
- except Exception:
55
- pass
56
- # INI
57
- try:
58
- cp = configparser.ConfigParser()
59
- cp.read_string(text)
60
- data = {s: dict(cp.items(s)) for s in cp.sections()}
61
- # also root-level keys under DEFAULT
62
- data.update(dict(cp.defaults()))
63
- self._cache = _flatten_obj(data)
64
- return
65
- except Exception:
66
- pass
67
- # dotenv (simple KEY=VALUE per line)
68
- try:
69
- kv = {}
70
- for line in text.splitlines():
71
- line = line.strip()
72
- if not line or line.startswith("#"):
73
- continue
74
- if "=" in line:
75
- k, v = line.split("=", 1)
76
- kv[k.strip()] = _strip_quotes(v.strip())
77
- self._cache = _flatten_obj(kv)
78
- if self._cache:
79
- return
80
- except Exception:
81
- pass
82
- # YAML if available
83
- try:
84
- import yaml # type: ignore
85
- data = yaml.safe_load(text) or {}
86
- self._cache = _flatten_obj(data)
87
- return
88
- except Exception:
89
- # if everything fails, fallback to empty (optional) or raise
90
- if self.optional:
91
- self._cache = {}
92
- return
93
- raise ValueError(f"Unrecognized file format: {self.path}")
94
-
95
- def get(self, key: str) -> Optional[str]:
96
- v = self._cache.get(key)
97
- return None if v is None else str(v)
98
-
99
- def keys(self) -> Iterable[str]:
100
- return self._cache.keys()
101
-
102
- # ---- Field specs (overrides) ----
103
-
104
- class FieldSpec:
105
- __slots__ = ("sources", "keys", "default", "path_is_dot")
106
- def __init__(self, *, sources: Tuple[str, ...], keys: Tuple[str, ...], default: Any, path_is_dot: bool):
107
- self.sources = sources
108
- self.keys = keys
109
- self.default = default
110
- self.path_is_dot = path_is_dot # true when keys are dotted-paths for structured files
111
-
112
- class _ValueSentinel:
113
- def __getitem__(self, key_default: str | Tuple[str, Any], /):
114
- if isinstance(key_default, tuple):
115
- key, default = key_default
116
- else:
117
- key, default = key_default, MISSING
118
- # default sources order env>file unless overridden in Value(...)
119
- return _ValueFactory(key, default)
120
- Value = _ValueSentinel()
121
-
122
- class _ValueFactory:
123
- def __init__(self, key: str, default: Any):
124
- self.key = key
125
- self.default = default
126
- def __call__(self, *, sources: Tuple[str, ...] = ("env","file")):
127
- return FieldSpec(sources=tuple(sources), keys=(self.key,), default=self.default, path_is_dot=False)
128
-
129
- class _EnvSentinel:
130
- def __getitem__(self, key_default: str | Tuple[str, Any], /):
131
- key, default = (key_default if isinstance(key_default, tuple) else (key_default, MISSING))
132
- return FieldSpec(sources=("env",), keys=(key,), default=default, path_is_dot=False)
133
- Env = _EnvSentinel()
134
-
135
- class _FileSentinel:
136
- def __getitem__(self, key_default: str | Tuple[str, Any], /):
137
- key, default = (key_default if isinstance(key_default, tuple) else (key_default, MISSING))
138
- return FieldSpec(sources=("file",), keys=(key,), default=default, path_is_dot=False)
139
- File = _FileSentinel()
140
-
141
- class _PathSentinel:
142
- class _FilePath:
143
- def __getitem__(self, key_default: str | Tuple[str, Any], /):
144
- key, default = (key_default if isinstance(key_default, tuple) else (key_default, MISSING))
145
- return FieldSpec(sources=("file",), keys=(key,), default=default, path_is_dot=True)
146
- file = _FilePath()
147
- Path = _PathSentinel()
148
-
149
- # ---- Class decorator ----
150
-
151
- def config_component(*, prefix: str = ""):
152
- def dec(cls):
153
- setattr(cls, _CONFIG_FLAG, True)
154
- setattr(cls, _CONFIG_PREFIX, prefix or "")
155
- if not hasattr(cls, _FIELD_META):
156
- setattr(cls, _FIELD_META, {})
157
- return cls
158
- return dec
159
-
160
- def is_config_component(cls: type) -> bool:
161
- return bool(getattr(cls, _CONFIG_FLAG, False))
162
-
163
- # ---- Registry / resolution ----
164
-
165
- class ConfigRegistry:
166
- """Holds ordered sources and provides typed resolution for @config_component classes."""
167
- def __init__(self, sources: Sequence[ConfigSource]):
168
- self.sources = tuple(sources or ())
169
-
170
- def resolve(self, keys: Iterable[str]) -> Optional[str]:
171
- # try each key across sources in order
172
- for key in keys:
173
- for src in self.sources:
174
- v = src.get(key)
175
- if v is not None:
176
- return v
177
- return None
178
-
179
- def register_field_spec(cls: type, name: str, spec: FieldSpec) -> None:
180
- meta: Dict[str, FieldSpec] = getattr(cls, _FIELD_META, None) or {}
181
- meta[name] = spec
182
- setattr(cls, _FIELD_META, meta)
183
-
184
- def build_component_instance(cls: type, registry: ConfigRegistry) -> Any:
185
- prefix = getattr(cls, _CONFIG_PREFIX, "")
186
- overrides: Dict[str, FieldSpec] = getattr(cls, _FIELD_META, {}) or {}
187
-
188
- if is_dataclass(cls):
189
- kwargs = {}
190
- for f in fields(cls):
191
- name = f.name
192
- spec = overrides.get(name)
193
- if spec:
194
- val = _resolve_with_spec(spec, registry)
195
- else:
196
- # auto: PREFIX+NAME or NAME (env), NAME (file)
197
- val = registry.resolve((prefix + name.upper(), name.upper()))
198
- if val is None and f.default is not MISSING:
199
- val = f.default
200
- elif val is None and f.default_factory is not MISSING: # type: ignore
201
- val = f.default_factory() # type: ignore
202
- if val is None and f.default is MISSING and getattr(f, "default_factory", MISSING) is MISSING: # type: ignore
203
- raise NameError(f"Missing config for field {cls.__name__}.{name}")
204
- kwargs[name] = _coerce_type(val, f.type)
205
- return cls(**kwargs)
206
-
207
- # Non-dataclass: inspect __init__ signature
208
- import inspect
209
- sig = inspect.signature(cls.__init__)
210
- hints = _get_type_hints_safe(cls.__init__, owner=cls)
211
- kwargs = {}
212
- for pname, par in sig.parameters.items():
213
- if pname == "self" or par.kind in (inspect.Parameter.VAR_POSITIONAL, inspect.Parameter.VAR_KEYWORD):
214
- continue
215
- ann = hints.get(pname, par.annotation)
216
- spec = overrides.get(pname)
217
- if spec:
218
- val = _resolve_with_spec(spec, registry)
219
- else:
220
- val = registry.resolve((prefix + pname.upper(), pname.upper()))
221
- if val is None and par.default is not inspect._empty:
222
- val = par.default
223
- if val is None and par.default is inspect._empty:
224
- raise NameError(f"Missing config for field {cls.__name__}.{pname}")
225
- kwargs[pname] = _coerce_type(val, ann)
226
- return cls(**kwargs)
227
-
228
- # ---- helpers ----
229
-
230
- def _resolve_with_spec(spec: FieldSpec, registry: ConfigRegistry) -> Any:
231
- # respect spec.sources ordering, but try all keys for each source
232
- for src_kind in spec.sources:
233
- if src_kind == "env":
234
- v = _resolve_from_sources(registry, spec.keys, predicate=lambda s: isinstance(s, EnvSource))
235
- elif src_kind == "file":
236
- if spec.path_is_dot:
237
- v = _resolve_path_from_files(registry, spec.keys)
238
- else:
239
- v = _resolve_from_sources(registry, spec.keys, predicate=lambda s: isinstance(s, FileSource))
240
- else:
241
- v = None
242
- if v is not None:
243
- return v
244
- return None if spec.default is MISSING else spec.default
245
-
246
- def _resolve_from_sources(registry: ConfigRegistry, keys: Tuple[str, ...], predicate: Callable[[ConfigSource], bool]) -> Optional[str]:
247
- for key in keys:
248
- for src in registry.sources:
249
- if predicate(src):
250
- v = src.get(key)
251
- if v is not None:
252
- return v
253
- return None
254
-
255
- def _resolve_path_from_files(registry: ConfigRegistry, dotted_keys: Tuple[str, ...]) -> Optional[str]:
256
- for key in dotted_keys:
257
- path = key.split(".")
258
- for src in registry.sources:
259
- if isinstance(src, FileSource):
260
- # FileSource caches flattened dict already
261
- v = src.get(key)
262
- if v is not None:
263
- return v
264
- return None
265
-
266
- def _flatten_obj(obj: Any, prefix: str = "") -> Dict[str, Any]:
267
- out: Dict[str, Any] = {}
268
- if isinstance(obj, dict):
269
- for k, v in obj.items():
270
- k2 = (prefix + "." + str(k)) if prefix else str(k)
271
- out.update(_flatten_obj(v, k2))
272
- elif isinstance(obj, (list, tuple)):
273
- for i, v in enumerate(obj):
274
- k2 = (prefix + "." + str(i)) if prefix else str(i)
275
- out.update(_flatten_obj(v, k2))
276
- else:
277
- out[prefix] = obj
278
- if "." in prefix:
279
- # also expose leaf as KEY without dots if single-segment? no; keep dotted only
280
- pass
281
- # also expose top-level KEY without dots when no prefix used:
282
- if prefix and "." not in prefix:
283
- out[prefix] = obj
284
- # Additionally mirror top-level simple keys as UPPERCASE for convenience
285
- if prefix and "." not in prefix:
286
- out[prefix.upper()] = obj
287
- return out
288
-
289
- def _strip_quotes(s: str) -> str:
290
- if (s.startswith('"') and s.endswith('"')) or (s.startswith("'") and s.endswith("'")):
291
- return s[1:-1]
292
- return s
293
-
294
- def _coerce_type(val: Any, ann: Any) -> Any:
295
- if val is None:
296
- return None
297
- # strings from sources come as str; coerce to basic types
298
- try:
299
- from typing import get_origin, get_args
300
- origin = get_origin(ann) or ann
301
- if origin in (int,):
302
- return int(val)
303
- if origin in (float,):
304
- return float(val)
305
- if origin in (bool,):
306
- s = str(val).strip().lower()
307
- if s in ("1","true","yes","y","on"): return True
308
- if s in ("0","false","no","n","off"): return False
309
- return bool(val)
310
- except Exception:
311
- pass
312
- return val
313
-
314
- def _get_type_hints_safe(fn, owner=None):
315
- try:
316
- import inspect
317
- mod = inspect.getmodule(fn)
318
- g = getattr(mod, "__dict__", {})
319
- l = vars(owner) if owner is not None else None
320
- from typing import get_type_hints
321
- return get_type_hints(fn, globalns=g, localns=l, include_extras=True)
322
- except Exception:
323
- return {}
324
-
325
- # ---- Public API helpers to be imported by users ----
326
-
327
- __all__ = [
328
- "config_component", "EnvSource", "FileSource",
329
- "Env", "File", "Path", "Value",
330
- "ConfigRegistry", "register_field_spec", "is_config_component",
331
- ]
332
-
pico_ioc/decorators.py DELETED
@@ -1,120 +0,0 @@
1
- from __future__ import annotations
2
- import functools
3
- from typing import Any, Iterable, Optional, Callable, Tuple
4
-
5
- COMPONENT_FLAG = "_is_component"
6
- COMPONENT_KEY = "_component_key"
7
- COMPONENT_LAZY = "_component_lazy"
8
-
9
- FACTORY_FLAG = "_is_factory_component"
10
- PROVIDES_KEY = "_provides_name"
11
- PROVIDES_LAZY = "_pico_lazy"
12
-
13
- PLUGIN_FLAG = "_is_pico_plugin"
14
- QUALIFIERS_KEY = "_pico_qualifiers"
15
-
16
- COMPONENT_TAGS = "_pico_tags"
17
- PROVIDES_TAGS = "_pico_tags"
18
-
19
- ON_MISSING_META = "_pico_on_missing"
20
- PRIMARY_FLAG = "_pico_primary"
21
- CONDITIONAL_META = "_pico_conditional"
22
-
23
- INFRA_META = "__pico_infrastructure__"
24
-
25
- def factory_component(cls):
26
- setattr(cls, FACTORY_FLAG, True)
27
- return cls
28
-
29
- def component(cls=None, *, name: Any = None, lazy: bool = False, tags: Iterable[str] = ()):
30
- def dec(c):
31
- setattr(c, COMPONENT_FLAG, True)
32
- setattr(c, COMPONENT_KEY, name if name is not None else c)
33
- setattr(c, COMPONENT_LAZY, bool(lazy))
34
- setattr(c, COMPONENT_TAGS, tuple(tags) if tags else ())
35
- return c
36
- return dec(cls) if cls else dec
37
-
38
- def provides(key: Any, *, lazy: bool = False, tags: Iterable[str] = ()):
39
- def dec(fn):
40
- @functools.wraps(fn)
41
- def w(*a, **k):
42
- return fn(*a, **k)
43
- setattr(w, PROVIDES_KEY, key)
44
- setattr(w, PROVIDES_LAZY, bool(lazy))
45
- setattr(w, PROVIDES_TAGS, tuple(tags) if tags else ())
46
- return w
47
- return dec
48
-
49
- def plugin(cls):
50
- setattr(cls, PLUGIN_FLAG, True)
51
- return cls
52
-
53
- class Qualifier(str):
54
- __slots__ = ()
55
-
56
- def qualifier(*qs: Qualifier):
57
- def dec(cls):
58
- current: Iterable[Qualifier] = getattr(cls, QUALIFIERS_KEY, ())
59
- seen = set(current)
60
- merged = list(current)
61
- for q in qs:
62
- if q not in seen:
63
- merged.append(q)
64
- seen.add(q)
65
- setattr(cls, QUALIFIERS_KEY, tuple(merged))
66
- return cls
67
- return dec
68
-
69
- def on_missing(selector: object, *, priority: int = 0):
70
- def dec(obj):
71
- setattr(obj, ON_MISSING_META, {"selector": selector, "priority": int(priority)})
72
- return obj
73
- return dec
74
-
75
- def primary(obj):
76
- setattr(obj, PRIMARY_FLAG, True)
77
- return obj
78
-
79
- def conditional(
80
- *,
81
- profiles: Tuple[str, ...] = (),
82
- require_env: Tuple[str, ...] = (),
83
- predicate: Optional[Callable[[], bool]] = None,
84
- ):
85
- def dec(obj):
86
- setattr(obj, CONDITIONAL_META, {
87
- "profiles": tuple(profiles),
88
- "require_env": tuple(require_env),
89
- "predicate": predicate,
90
- })
91
- return obj
92
- return dec
93
-
94
- def infrastructure(
95
- _cls=None, *, order: int = 0,
96
- profiles: Tuple[str, ...] = (),
97
- require_env: Tuple[str, ...] = (),
98
- predicate: Optional[Callable[[], bool]] = None,
99
- ):
100
- def dec(cls):
101
- setattr(cls, INFRA_META, {
102
- "order": int(order),
103
- "profiles": tuple(profiles),
104
- "require_env": tuple(require_env),
105
- "predicate": predicate,
106
- })
107
- return cls
108
- return dec if _cls is None else dec(_cls)
109
-
110
- __all__ = [
111
- "component", "factory_component", "provides", "plugin",
112
- "Qualifier", "qualifier",
113
- "on_missing", "primary", "conditional", "infrastructure",
114
- "COMPONENT_FLAG", "COMPONENT_KEY", "COMPONENT_LAZY",
115
- "FACTORY_FLAG", "PROVIDES_KEY", "PROVIDES_LAZY",
116
- "PLUGIN_FLAG", "QUALIFIERS_KEY", "COMPONENT_TAGS", "PROVIDES_TAGS",
117
- "ON_MISSING_META", "PRIMARY_FLAG", "CONDITIONAL_META",
118
- "INFRA_META",
119
- ]
120
-