xtr-dependency-injection 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.
- xtr_dependency_injection/__init__.py +72 -0
- xtr_dependency_injection/builder/__init__.py +7 -0
- xtr_dependency_injection/builder/autoconfigurator.py +71 -0
- xtr_dependency_injection/builder/conflict_policy.py +94 -0
- xtr_dependency_injection/builder/container_builder.py +125 -0
- xtr_dependency_injection/builder/definition.py +67 -0
- xtr_dependency_injection/builder/service_configurator.py +232 -0
- xtr_dependency_injection/bundle/__init__.py +9 -0
- xtr_dependency_injection/bundle/as_bundle.py +110 -0
- xtr_dependency_injection/bundle/bundle.py +93 -0
- xtr_dependency_injection/bundle/bundle_metadata.py +29 -0
- xtr_dependency_injection/compiler/__init__.py +9 -0
- xtr_dependency_injection/compiler/_wireup_bridge.py +114 -0
- xtr_dependency_injection/compiler/registration.py +347 -0
- xtr_dependency_injection/compiler/wireup_compiler.py +201 -0
- xtr_dependency_injection/config/__init__.py +9 -0
- xtr_dependency_injection/config/config_prepender.py +88 -0
- xtr_dependency_injection/config/config_provider.py +129 -0
- xtr_dependency_injection/config/config_resolver.py +235 -0
- xtr_dependency_injection/config/configure.py +61 -0
- xtr_dependency_injection/config/env.py +80 -0
- xtr_dependency_injection/config/parameters.py +99 -0
- xtr_dependency_injection/decorator/__init__.py +21 -0
- xtr_dependency_injection/decorator/_marker.py +27 -0
- xtr_dependency_injection/decorator/as_decorator.py +116 -0
- xtr_dependency_injection/decorator/compiler_pass.py +45 -0
- xtr_dependency_injection/decorator/exclude.py +23 -0
- xtr_dependency_injection/decorator/lifecycle.py +64 -0
- xtr_dependency_injection/decorator/when.py +65 -0
- xtr_dependency_injection/diagnostics/__init__.py +7 -0
- xtr_dependency_injection/diagnostics/report.py +217 -0
- xtr_dependency_injection/discovery/__init__.py +7 -0
- xtr_dependency_injection/discovery/bundle_discovery.py +84 -0
- xtr_dependency_injection/discovery/bundle_resolver.py +302 -0
- xtr_dependency_injection/exception/__init__.py +51 -0
- xtr_dependency_injection/exception/_naming.py +32 -0
- xtr_dependency_injection/exception/builder_frozen_error.py +18 -0
- xtr_dependency_injection/exception/builder_phase_error.py +24 -0
- xtr_dependency_injection/exception/bundle_definition_error.py +25 -0
- xtr_dependency_injection/exception/circular_bundle_dependency_error.py +18 -0
- xtr_dependency_injection/exception/config_provider_error.py +25 -0
- xtr_dependency_injection/exception/conflicting_config_providers_error.py +23 -0
- xtr_dependency_injection/exception/decorator_signature_error.py +20 -0
- xtr_dependency_injection/exception/dependency_injection_error.py +14 -0
- xtr_dependency_injection/exception/duplicate_bundle_error.py +26 -0
- xtr_dependency_injection/exception/duplicate_service_error.py +35 -0
- xtr_dependency_injection/exception/invalid_environment_error.py +20 -0
- xtr_dependency_injection/exception/invalid_environment_variable_error.py +28 -0
- xtr_dependency_injection/exception/kernel_already_booted_error.py +18 -0
- xtr_dependency_injection/exception/missing_bundle_error.py +24 -0
- xtr_dependency_injection/exception/missing_environment_variable_error.py +18 -0
- xtr_dependency_injection/exception/parameter_conflict_error.py +22 -0
- xtr_dependency_injection/exception/resource_import_error.py +18 -0
- xtr_dependency_injection/exception/unknown_config_type_error.py +32 -0
- xtr_dependency_injection/exception/unknown_locator_key_error.py +26 -0
- xtr_dependency_injection/exception/unknown_service_error.py +26 -0
- xtr_dependency_injection/kernel/__init__.py +10 -0
- xtr_dependency_injection/kernel/booted_kernel.py +106 -0
- xtr_dependency_injection/kernel/compiled_kernel.py +102 -0
- xtr_dependency_injection/kernel/kernel.py +563 -0
- xtr_dependency_injection/kernel/kernel_bundle.py +63 -0
- xtr_dependency_injection/kernel/kernel_interface.py +109 -0
- xtr_dependency_injection/py.typed +0 -0
- xtr_dependency_injection/runtime/__init__.py +9 -0
- xtr_dependency_injection/runtime/bind_callable.py +143 -0
- xtr_dependency_injection/runtime/service_locator.py +60 -0
- xtr_dependency_injection/runtime/services_resetter.py +63 -0
- xtr_dependency_injection/scan/__init__.py +7 -0
- xtr_dependency_injection/scan/default_excludes.py +21 -0
- xtr_dependency_injection/scan/scanned_object.py +26 -0
- xtr_dependency_injection/scan/scanner.py +192 -0
- xtr_dependency_injection/standalone.py +100 -0
- xtr_dependency_injection/testing/__init__.py +57 -0
- xtr_dependency_injection/testing/pytest_plugin.py +54 -0
- xtr_dependency_injection-1.0.0.dist-info/METADATA +469 -0
- xtr_dependency_injection-1.0.0.dist-info/RECORD +78 -0
- xtr_dependency_injection-1.0.0.dist-info/WHEEL +4 -0
- xtr_dependency_injection-1.0.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""A Symfony-style bundle and kernel layer for the xtr libraries, built on wireup.
|
|
2
|
+
|
|
3
|
+
Each library ships one bundle; installed bundles register themselves; an
|
|
4
|
+
application is one ``Kernel("app")`` line plus decorators. What comes out is a
|
|
5
|
+
plain wireup ``AsyncContainer``: this package decides *what* goes into it, in
|
|
6
|
+
*which order*, *for which environment*, and runs the lifecycle around it.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from importlib.metadata import version
|
|
12
|
+
|
|
13
|
+
from .builder import Definition, Origin, ServiceKey
|
|
14
|
+
from .builder.container_builder import ContainerBuilder
|
|
15
|
+
from .builder.service_configurator import ServiceConfigurator
|
|
16
|
+
from .bundle import Bundle, BundleMetadata, NoConfig, as_bundle
|
|
17
|
+
from .config import configure, env, parameters
|
|
18
|
+
from .config.config_prepender import ConfigPrepender
|
|
19
|
+
from .decorator import (
|
|
20
|
+
Inner,
|
|
21
|
+
as_decorator,
|
|
22
|
+
compiler_pass,
|
|
23
|
+
exclude,
|
|
24
|
+
on_boot,
|
|
25
|
+
on_shutdown,
|
|
26
|
+
when,
|
|
27
|
+
when_not,
|
|
28
|
+
)
|
|
29
|
+
from .diagnostics import KernelReport
|
|
30
|
+
from .discovery import DiscoveredBundle, discover_bundles
|
|
31
|
+
from .kernel import BootedKernel, CompiledKernel, Kernel, KernelInterface
|
|
32
|
+
from .runtime import ServiceLocator, ServicesResetter, bind_callable
|
|
33
|
+
from .scan import DEFAULT_EXCLUDES
|
|
34
|
+
from .standalone import injectables
|
|
35
|
+
|
|
36
|
+
__all__ = [
|
|
37
|
+
"DEFAULT_EXCLUDES",
|
|
38
|
+
"BootedKernel",
|
|
39
|
+
"Bundle",
|
|
40
|
+
"BundleMetadata",
|
|
41
|
+
"CompiledKernel",
|
|
42
|
+
"ConfigPrepender",
|
|
43
|
+
"ContainerBuilder",
|
|
44
|
+
"Definition",
|
|
45
|
+
"DiscoveredBundle",
|
|
46
|
+
"Inner",
|
|
47
|
+
"Kernel",
|
|
48
|
+
"KernelInterface",
|
|
49
|
+
"KernelReport",
|
|
50
|
+
"NoConfig",
|
|
51
|
+
"Origin",
|
|
52
|
+
"ServiceConfigurator",
|
|
53
|
+
"ServiceKey",
|
|
54
|
+
"ServiceLocator",
|
|
55
|
+
"ServicesResetter",
|
|
56
|
+
"as_bundle",
|
|
57
|
+
"as_decorator",
|
|
58
|
+
"bind_callable",
|
|
59
|
+
"compiler_pass",
|
|
60
|
+
"configure",
|
|
61
|
+
"discover_bundles",
|
|
62
|
+
"env",
|
|
63
|
+
"exclude",
|
|
64
|
+
"injectables",
|
|
65
|
+
"on_boot",
|
|
66
|
+
"on_shutdown",
|
|
67
|
+
"parameters",
|
|
68
|
+
"when",
|
|
69
|
+
"when_not",
|
|
70
|
+
]
|
|
71
|
+
|
|
72
|
+
__version__ = version("xtr-dependency-injection")
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""Autoconfiguration: a bundle reacting to what the scan found.
|
|
2
|
+
|
|
3
|
+
A bundle registers a *reader* — metadata found on a candidate, empty for no
|
|
4
|
+
match — and an *apply*, called once per metadata item. The console bundle
|
|
5
|
+
reads ``@as_command`` descriptors and registers each command; the scan never
|
|
6
|
+
needs to know what a command is.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
from typing import TYPE_CHECKING, Any, TypeAlias
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from collections.abc import Callable, Iterable, Sequence
|
|
16
|
+
|
|
17
|
+
from xtr_dependency_injection.scan.scanned_object import ScannedObject
|
|
18
|
+
|
|
19
|
+
from .service_configurator import ServiceConfigurator
|
|
20
|
+
|
|
21
|
+
__all__ = ["Apply", "Autoconfigurator", "Reader", "run_autoconfigurators"]
|
|
22
|
+
|
|
23
|
+
# What a reader finds is only meaningful to the bundle that reads it, so the
|
|
24
|
+
# builder passes it through untyped.
|
|
25
|
+
Reader: TypeAlias = "Callable[[object], Iterable[Any]]" # pyright: ignore[reportExplicitAny]
|
|
26
|
+
Apply: TypeAlias = "Callable[[object, Any, ServiceConfigurator], None]" # pyright: ignore[reportExplicitAny]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass(frozen=True, slots=True)
|
|
30
|
+
class Autoconfigurator:
|
|
31
|
+
"""One ``services.autoconfigure(reader, apply)`` call, and the bundle that made it."""
|
|
32
|
+
|
|
33
|
+
owner: str
|
|
34
|
+
reader: Reader
|
|
35
|
+
apply: Apply
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def run_autoconfigurators(
|
|
39
|
+
autoconfigurators: Sequence[Autoconfigurator],
|
|
40
|
+
candidates: Sequence[ScannedObject],
|
|
41
|
+
configurator_for: Callable[[str, ScannedObject], ServiceConfigurator],
|
|
42
|
+
) -> None:
|
|
43
|
+
"""Run every autoconfigurator over every candidate.
|
|
44
|
+
|
|
45
|
+
The outer loop is over autoconfigurators, in bundle order and then
|
|
46
|
+
registration order; the inner loop over candidates, in scan order.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
autoconfigurators: What bundles registered while loading.
|
|
50
|
+
candidates: The service candidates of every scan.
|
|
51
|
+
configurator_for: The configurator ``apply`` receives, for a bundle
|
|
52
|
+
and the candidate it applies to.
|
|
53
|
+
|
|
54
|
+
Raises:
|
|
55
|
+
Exception: Whatever a reader raised — a reader failing is a bug —
|
|
56
|
+
with a note naming the reader and the candidate.
|
|
57
|
+
"""
|
|
58
|
+
for autoconfigurator in autoconfigurators:
|
|
59
|
+
for candidate in candidates:
|
|
60
|
+
try:
|
|
61
|
+
found = tuple(autoconfigurator.reader(candidate.obj))
|
|
62
|
+
except Exception as error:
|
|
63
|
+
reader = getattr(autoconfigurator.reader, "__qualname__", "reader")
|
|
64
|
+
where = f"of bundle {autoconfigurator.owner} on {candidate.name}"
|
|
65
|
+
error.add_note(f"raised by autoconfigure reader {reader} {where}")
|
|
66
|
+
raise
|
|
67
|
+
if not found:
|
|
68
|
+
continue
|
|
69
|
+
services = configurator_for(autoconfigurator.owner, candidate)
|
|
70
|
+
for metadata in found: # pyright: ignore[reportAny]
|
|
71
|
+
autoconfigurator.apply(candidate.obj, metadata, services)
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"""Who wins when two definitions claim one key.
|
|
2
|
+
|
|
3
|
+
Checked before wireup ever sees the definitions, so an error names both
|
|
4
|
+
origins instead of a bare duplicate:
|
|
5
|
+
|
|
6
|
+
- the application over a bundle: the application wins, silently, and the
|
|
7
|
+
report records the override (Symfony's parity);
|
|
8
|
+
- bundle against bundle: an error, unless it goes through
|
|
9
|
+
``builder.replace``;
|
|
10
|
+
- application against application: an error.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from dataclasses import replace
|
|
16
|
+
from typing import TYPE_CHECKING, final
|
|
17
|
+
|
|
18
|
+
from xtr_dependency_injection.exception import DuplicateServiceError
|
|
19
|
+
|
|
20
|
+
if TYPE_CHECKING:
|
|
21
|
+
from .definition import Definition, Origin, ServiceKey
|
|
22
|
+
|
|
23
|
+
__all__ = ["DefinitionStore"]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@final
|
|
27
|
+
class DefinitionStore:
|
|
28
|
+
"""Every definition of one build, by key, in declaration order."""
|
|
29
|
+
|
|
30
|
+
__slots__ = ("_counter", "_definitions", "_overrides", "_sequence")
|
|
31
|
+
|
|
32
|
+
def __init__(self) -> None:
|
|
33
|
+
"""Start empty."""
|
|
34
|
+
self._definitions: dict[ServiceKey, Definition] = {}
|
|
35
|
+
self._overrides: dict[ServiceKey, list[Origin]] = {}
|
|
36
|
+
self._sequence: dict[ServiceKey, int] = {}
|
|
37
|
+
self._counter = 0
|
|
38
|
+
|
|
39
|
+
def add(self, definition: Definition) -> None:
|
|
40
|
+
"""Add ``definition``, applying the conflict policy.
|
|
41
|
+
|
|
42
|
+
Raises:
|
|
43
|
+
DuplicateServiceError: If neither definition may override the
|
|
44
|
+
other.
|
|
45
|
+
"""
|
|
46
|
+
key = definition.key
|
|
47
|
+
existing = self._definitions.get(key)
|
|
48
|
+
if existing is None:
|
|
49
|
+
self._insert(definition)
|
|
50
|
+
return
|
|
51
|
+
if definition.origin.kind == "app" and existing.origin.kind != "app":
|
|
52
|
+
self._overrides.setdefault(key, []).append(existing.origin)
|
|
53
|
+
self._insert(definition)
|
|
54
|
+
elif existing.origin.kind == "app" and definition.origin.kind != "app":
|
|
55
|
+
self._overrides.setdefault(key, []).append(definition.origin)
|
|
56
|
+
else:
|
|
57
|
+
raise DuplicateServiceError(key, existing.origin, definition.origin)
|
|
58
|
+
|
|
59
|
+
def replace(self, definition: Definition) -> None:
|
|
60
|
+
"""Replace the definition of ``definition.key`` on purpose, keeping its position."""
|
|
61
|
+
existing = self._definitions[definition.key]
|
|
62
|
+
self._overrides.setdefault(definition.key, []).append(existing.origin)
|
|
63
|
+
self._definitions[definition.key] = definition
|
|
64
|
+
|
|
65
|
+
def update(self, key: ServiceKey, **changes: object) -> None:
|
|
66
|
+
"""Change fields of the definition of ``key`` without it counting as an override."""
|
|
67
|
+
self._definitions[key] = replace(self._definitions[key], **changes)
|
|
68
|
+
|
|
69
|
+
def remove(self, key: ServiceKey) -> None:
|
|
70
|
+
"""Forget the definition of ``key``."""
|
|
71
|
+
del self._definitions[key]
|
|
72
|
+
_ = self._sequence.pop(key, None)
|
|
73
|
+
|
|
74
|
+
def get(self, key: ServiceKey) -> Definition | None:
|
|
75
|
+
"""Return the definition of ``key``, or ``None``."""
|
|
76
|
+
return self._definitions.get(key)
|
|
77
|
+
|
|
78
|
+
def has_provider(self, provider: object) -> bool:
|
|
79
|
+
"""Return whether any definition is provided by this very object."""
|
|
80
|
+
return any(definition.provider is provider for definition in self._definitions.values())
|
|
81
|
+
|
|
82
|
+
def definitions(self) -> tuple[Definition, ...]:
|
|
83
|
+
"""Return every definition, in declaration order."""
|
|
84
|
+
ordered = sorted(self._definitions.values(), key=lambda d: self._sequence[d.key])
|
|
85
|
+
return tuple(ordered)
|
|
86
|
+
|
|
87
|
+
def overrides_of(self, key: ServiceKey) -> tuple[Origin, ...]:
|
|
88
|
+
"""Return the origins of the definitions ``key``'s replaced or outranked."""
|
|
89
|
+
return tuple(self._overrides.get(key, ()))
|
|
90
|
+
|
|
91
|
+
def _insert(self, definition: Definition) -> None:
|
|
92
|
+
self._counter += 1
|
|
93
|
+
self._definitions[definition.key] = definition
|
|
94
|
+
self._sequence[definition.key] = self._counter
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"""What ``Bundle.process`` and ``@compiler_pass`` receive: every definition, still mutable.
|
|
2
|
+
|
|
3
|
+
By now every bundle has loaded and autoconfiguration has run, so a pass sees
|
|
4
|
+
the whole container — what exists, which config each bundle resolved — and
|
|
5
|
+
may still add, replace, remove or decorate.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from typing import TYPE_CHECKING, TypeVar, cast, final, overload
|
|
11
|
+
|
|
12
|
+
from xtr_dependency_injection.exception import MissingBundleError, UnknownConfigTypeError
|
|
13
|
+
|
|
14
|
+
from .definition import Definition, Lifetime
|
|
15
|
+
from .service_configurator import DecorationRequest, Kind, ServiceConfigurator, kind_of
|
|
16
|
+
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
from collections.abc import Callable, Hashable
|
|
19
|
+
|
|
20
|
+
__all__ = ["ContainerBuilder"]
|
|
21
|
+
|
|
22
|
+
C = TypeVar("C")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@final
|
|
26
|
+
class ContainerBuilder(ServiceConfigurator):
|
|
27
|
+
"""A whole-container view for processing, on behalf of one bundle or compiler pass.
|
|
28
|
+
|
|
29
|
+
``scan`` and ``autoconfigure`` are refused here: both have already run.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
def has(self, as_type: type, /, qualifier: Hashable | None = None) -> bool:
|
|
33
|
+
"""Return whether a service is defined for ``(as_type, qualifier)``."""
|
|
34
|
+
return self._state.store.get((as_type, qualifier)) is not None
|
|
35
|
+
|
|
36
|
+
def definitions(self, as_type: type | None = None, /) -> tuple[Definition, ...]:
|
|
37
|
+
"""Return every definition, or those provided under ``as_type``, in declaration order."""
|
|
38
|
+
every = self._state.store.definitions()
|
|
39
|
+
return every if as_type is None else tuple(d for d in every if d.key[0] is as_type)
|
|
40
|
+
|
|
41
|
+
@overload
|
|
42
|
+
def config_of(self, bundle: str, /) -> object: ...
|
|
43
|
+
@overload
|
|
44
|
+
def config_of(self, bundle: type[C], /) -> C: ...
|
|
45
|
+
def config_of(self, bundle: str | type[C], /) -> object:
|
|
46
|
+
"""Return the resolved config of a bundle, by name or by config type.
|
|
47
|
+
|
|
48
|
+
Raises:
|
|
49
|
+
MissingBundleError: If no active bundle has that name.
|
|
50
|
+
UnknownConfigTypeError: If no active bundle has that config type.
|
|
51
|
+
"""
|
|
52
|
+
configs = self._state.configs
|
|
53
|
+
if isinstance(bundle, str):
|
|
54
|
+
if bundle not in configs:
|
|
55
|
+
raise MissingBundleError(bundle, str(self._origin), "it is not active")
|
|
56
|
+
return configs[bundle]
|
|
57
|
+
for value in configs.values():
|
|
58
|
+
if type(value) is bundle:
|
|
59
|
+
return cast("C", value)
|
|
60
|
+
raise UnknownConfigTypeError(f"config_of by {self._origin}", bundle, None)
|
|
61
|
+
|
|
62
|
+
def replace(
|
|
63
|
+
self,
|
|
64
|
+
as_type: type,
|
|
65
|
+
provider: object,
|
|
66
|
+
/,
|
|
67
|
+
*,
|
|
68
|
+
qualifier: Hashable | None = None,
|
|
69
|
+
kind: Kind | None = None,
|
|
70
|
+
lifetime: Lifetime | None = None,
|
|
71
|
+
) -> None:
|
|
72
|
+
"""Replace the service ``(as_type, qualifier)`` with ``provider``, on purpose.
|
|
73
|
+
|
|
74
|
+
The kind is inferred from ``provider`` unless given; the lifetime, and
|
|
75
|
+
the position in collections, stay the replaced definition's.
|
|
76
|
+
|
|
77
|
+
Raises:
|
|
78
|
+
UnknownServiceError: If no such service is defined.
|
|
79
|
+
"""
|
|
80
|
+
self._allow("replace", "process")
|
|
81
|
+
existing = self._require((as_type, qualifier), "replace")
|
|
82
|
+
self._state.store.replace(
|
|
83
|
+
Definition(
|
|
84
|
+
key=existing.key,
|
|
85
|
+
provider=provider,
|
|
86
|
+
kind=kind if kind is not None else kind_of(provider),
|
|
87
|
+
lifetime=lifetime if lifetime is not None else existing.lifetime,
|
|
88
|
+
origin=self._origin,
|
|
89
|
+
priority=existing.priority,
|
|
90
|
+
reset_method=existing.reset_method,
|
|
91
|
+
)
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
def remove(self, as_type: type, /, *, qualifier: Hashable | None = None) -> None:
|
|
95
|
+
"""Remove the service ``(as_type, qualifier)``.
|
|
96
|
+
|
|
97
|
+
Raises:
|
|
98
|
+
UnknownServiceError: If no such service is defined.
|
|
99
|
+
"""
|
|
100
|
+
self._allow("remove", "process")
|
|
101
|
+
definition = self._require((as_type, qualifier), "remove")
|
|
102
|
+
self._state.store.remove(definition.key)
|
|
103
|
+
|
|
104
|
+
def decorate(
|
|
105
|
+
self,
|
|
106
|
+
as_type: type,
|
|
107
|
+
decorator: type | Callable[..., object],
|
|
108
|
+
/,
|
|
109
|
+
*,
|
|
110
|
+
qualifier: Hashable | None = None,
|
|
111
|
+
priority: int = 0,
|
|
112
|
+
) -> None:
|
|
113
|
+
"""Decorate the service ``(as_type, qualifier)`` with ``decorator``.
|
|
114
|
+
|
|
115
|
+
The decorator takes the service's place, and receives it through its
|
|
116
|
+
one ``Inner[as_type]`` parameter.
|
|
117
|
+
|
|
118
|
+
Raises:
|
|
119
|
+
UnknownServiceError: If no such service is defined.
|
|
120
|
+
"""
|
|
121
|
+
self._allow("decorate", "process")
|
|
122
|
+
key = self._require((as_type, qualifier), "decorate").key
|
|
123
|
+
name = f"{getattr(decorator, '__module__', '?')}:{getattr(decorator, '__qualname__', '?')}"
|
|
124
|
+
order = len(self._state.decorations)
|
|
125
|
+
self._state.decorations.append(DecorationRequest(key, decorator, priority, name, order))
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""What the builder holds: one definition per service key, and where each came from."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Hashable
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from typing import Literal, TypeAlias
|
|
8
|
+
|
|
9
|
+
from typing_extensions import override
|
|
10
|
+
|
|
11
|
+
__all__ = ["Definition", "Lifetime", "Origin", "ServiceKey"]
|
|
12
|
+
|
|
13
|
+
ServiceKey: TypeAlias = tuple[type, Hashable | None]
|
|
14
|
+
"""``(provided type, qualifier)`` — the identity wireup registers a service under."""
|
|
15
|
+
|
|
16
|
+
Lifetime: TypeAlias = Literal["singleton", "scoped", "transient"]
|
|
17
|
+
"""How long wireup keeps what a definition builds."""
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass(frozen=True, slots=True)
|
|
21
|
+
class Origin:
|
|
22
|
+
"""Who contributed a definition: the kernel, a bundle, or the application.
|
|
23
|
+
|
|
24
|
+
Attributes:
|
|
25
|
+
kind: ``"kernel"``, ``"bundle"`` or ``"app"``.
|
|
26
|
+
name: The bundle's name, or ``module:qualname`` of the application
|
|
27
|
+
object.
|
|
28
|
+
note: How it got there, when not directly — e.g.
|
|
29
|
+
``"via autoconfigure of app.x:Y"``.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
kind: Literal["kernel", "bundle", "app"]
|
|
33
|
+
name: str
|
|
34
|
+
note: str | None = None
|
|
35
|
+
|
|
36
|
+
@override
|
|
37
|
+
def __str__(self) -> str:
|
|
38
|
+
described = f"{self.kind} {self.name}"
|
|
39
|
+
return described if self.note is None else f"{described} ({self.note})"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclass(frozen=True, slots=True)
|
|
43
|
+
class Definition:
|
|
44
|
+
"""One service as the builder knows it, before it is compiled for wireup.
|
|
45
|
+
|
|
46
|
+
Attributes:
|
|
47
|
+
key: The type and qualifier it is provided under.
|
|
48
|
+
provider: The class, factory function or instance that builds it.
|
|
49
|
+
kind: How ``provider`` is registered: ``"class"``, ``"factory"``,
|
|
50
|
+
``"instance"``, or ``"declared"`` for an object already marked
|
|
51
|
+
with wireup's ``@injectable``.
|
|
52
|
+
lifetime: How long wireup keeps what it builds.
|
|
53
|
+
origin: Who contributed it.
|
|
54
|
+
priority: Its position among definitions of one origin group:
|
|
55
|
+
higher comes first in ``Sequence[T]`` and ``Mapping``
|
|
56
|
+
collections.
|
|
57
|
+
reset_method: The method ``ServicesResetter`` calls on it, when it is
|
|
58
|
+
resettable.
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
key: ServiceKey
|
|
62
|
+
provider: object
|
|
63
|
+
kind: Literal["class", "factory", "instance", "declared"]
|
|
64
|
+
lifetime: Lifetime
|
|
65
|
+
origin: Origin
|
|
66
|
+
priority: int = 0
|
|
67
|
+
reset_method: str | None = None
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
"""What ``Bundle.load`` receives: a bundle's view of the services being defined.
|
|
2
|
+
|
|
3
|
+
One configurator per bundle, so every definition knows its origin. Each
|
|
4
|
+
operation belongs to phases: ``scan`` and ``autoconfigure`` only mean
|
|
5
|
+
something while bundles load — scanning and autoconfiguration run right
|
|
6
|
+
after — and nothing may change once the container is compiled.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import inspect
|
|
12
|
+
from collections.abc import Callable, Hashable, Mapping
|
|
13
|
+
from dataclasses import dataclass, field
|
|
14
|
+
from types import ModuleType
|
|
15
|
+
from typing import Literal
|
|
16
|
+
|
|
17
|
+
from xtr_dependency_injection.compiler._wireup_bridge import declaration_of, key_type
|
|
18
|
+
from xtr_dependency_injection.exception import (
|
|
19
|
+
BuilderFrozenError,
|
|
20
|
+
BuilderPhaseError,
|
|
21
|
+
UnknownServiceError,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
from .autoconfigurator import Apply, Autoconfigurator, Reader
|
|
25
|
+
from .conflict_policy import DefinitionStore
|
|
26
|
+
from .definition import Definition, Lifetime, Origin, ServiceKey
|
|
27
|
+
|
|
28
|
+
__all__ = [
|
|
29
|
+
"BuildState",
|
|
30
|
+
"DecorationRequest",
|
|
31
|
+
"ResettableRequest",
|
|
32
|
+
"ServiceConfigurator",
|
|
33
|
+
"kind_of",
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
Phase = Literal["load", "autoconfigure", "process", "frozen"]
|
|
37
|
+
Kind = Literal["class", "factory", "instance", "declared"]
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass(frozen=True, slots=True)
|
|
41
|
+
class ResettableRequest:
|
|
42
|
+
"""A ``resettable()`` call, resolved against the final definitions."""
|
|
43
|
+
|
|
44
|
+
key: ServiceKey
|
|
45
|
+
method: str
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@dataclass(frozen=True, slots=True)
|
|
49
|
+
class DecorationRequest:
|
|
50
|
+
"""A decoration asked for by ``builder.decorate`` or ``@as_decorator``."""
|
|
51
|
+
|
|
52
|
+
key: ServiceKey
|
|
53
|
+
decorator: type | Callable[..., object]
|
|
54
|
+
priority: int
|
|
55
|
+
name: str
|
|
56
|
+
order: int
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@dataclass(slots=True)
|
|
60
|
+
class BuildState:
|
|
61
|
+
"""Everything one build's configurators and builders share.
|
|
62
|
+
|
|
63
|
+
Attributes:
|
|
64
|
+
env: The environment being built.
|
|
65
|
+
debug: Whether the kernel runs in debug mode.
|
|
66
|
+
bundles: Active bundle names, in dependency order.
|
|
67
|
+
configs: Resolved config by bundle name.
|
|
68
|
+
phase: The current phase.
|
|
69
|
+
store: The definitions.
|
|
70
|
+
late_scans: Resources ``scan()`` asked for, with the asking bundle.
|
|
71
|
+
autoconfigurators: What ``autoconfigure()`` registered.
|
|
72
|
+
parameters: Parameters bundles contributed, with their source.
|
|
73
|
+
resettables: ``resettable()`` requests.
|
|
74
|
+
decorations: Decoration requests.
|
|
75
|
+
"""
|
|
76
|
+
|
|
77
|
+
env: str
|
|
78
|
+
debug: bool
|
|
79
|
+
bundles: tuple[str, ...]
|
|
80
|
+
configs: Mapping[str, object]
|
|
81
|
+
phase: Phase = "load"
|
|
82
|
+
store: DefinitionStore = field(default_factory=DefinitionStore)
|
|
83
|
+
late_scans: list[tuple[str, tuple[str | ModuleType, ...]]] = field(default_factory=list)
|
|
84
|
+
autoconfigurators: list[Autoconfigurator] = field(default_factory=list)
|
|
85
|
+
parameters: list[tuple[str, Mapping[str, object]]] = field(default_factory=list)
|
|
86
|
+
resettables: list[ResettableRequest] = field(default_factory=list)
|
|
87
|
+
decorations: list[DecorationRequest] = field(default_factory=list)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class ServiceConfigurator:
|
|
91
|
+
"""Defines services on behalf of one bundle.
|
|
92
|
+
|
|
93
|
+
Attributes:
|
|
94
|
+
env: The environment being built.
|
|
95
|
+
debug: Whether the kernel runs in debug mode.
|
|
96
|
+
"""
|
|
97
|
+
|
|
98
|
+
__slots__: tuple[str, ...] = ("_origin", "_state", "debug", "env")
|
|
99
|
+
|
|
100
|
+
env: str
|
|
101
|
+
debug: bool
|
|
102
|
+
_state: BuildState
|
|
103
|
+
_origin: Origin
|
|
104
|
+
|
|
105
|
+
def __init__(self, state: BuildState, origin: Origin) -> None:
|
|
106
|
+
"""Define services into ``state``, as ``origin``."""
|
|
107
|
+
self._state = state
|
|
108
|
+
self._origin = origin
|
|
109
|
+
self.env = state.env
|
|
110
|
+
self.debug = state.debug
|
|
111
|
+
|
|
112
|
+
def has_bundle(self, name: str) -> bool:
|
|
113
|
+
"""Return whether the bundle ``name`` is active in this build."""
|
|
114
|
+
return name in self._state.bundles
|
|
115
|
+
|
|
116
|
+
def instance(
|
|
117
|
+
self,
|
|
118
|
+
obj: object,
|
|
119
|
+
/,
|
|
120
|
+
*,
|
|
121
|
+
as_type: type | None = None,
|
|
122
|
+
qualifier: Hashable | None = None,
|
|
123
|
+
priority: int = 0,
|
|
124
|
+
) -> None:
|
|
125
|
+
"""Provide ``obj`` itself, under ``as_type`` (default: its own type)."""
|
|
126
|
+
self._allow("instance", "load", "autoconfigure", "process")
|
|
127
|
+
key = (as_type if as_type is not None else type(obj), qualifier)
|
|
128
|
+
self._add(key, obj, "instance", "singleton", priority)
|
|
129
|
+
|
|
130
|
+
def factory(
|
|
131
|
+
self,
|
|
132
|
+
fn: Callable[..., object],
|
|
133
|
+
/,
|
|
134
|
+
*,
|
|
135
|
+
lifetime: Lifetime = "singleton",
|
|
136
|
+
as_type: type | None = None,
|
|
137
|
+
qualifier: Hashable | None = None,
|
|
138
|
+
priority: int = 0,
|
|
139
|
+
) -> None:
|
|
140
|
+
"""Provide what ``fn`` returns — or yields, cleaned up after — under its return type.
|
|
141
|
+
|
|
142
|
+
Raises:
|
|
143
|
+
TypeError: If ``fn`` is not a plain function.
|
|
144
|
+
FactoryReturnTypeIsEmptyError: If ``fn`` has no return
|
|
145
|
+
annotation.
|
|
146
|
+
"""
|
|
147
|
+
self._allow("factory", "load", "autoconfigure", "process")
|
|
148
|
+
if not inspect.isfunction(fn):
|
|
149
|
+
msg = f"factory() takes a function, not {fn!r}"
|
|
150
|
+
raise TypeError(msg)
|
|
151
|
+
self._add((key_type(fn, as_type), qualifier), fn, "factory", lifetime, priority)
|
|
152
|
+
|
|
153
|
+
def service(
|
|
154
|
+
self,
|
|
155
|
+
cls: type,
|
|
156
|
+
/,
|
|
157
|
+
*,
|
|
158
|
+
lifetime: Lifetime = "singleton",
|
|
159
|
+
as_type: type | None = None,
|
|
160
|
+
qualifier: Hashable | None = None,
|
|
161
|
+
priority: int = 0,
|
|
162
|
+
) -> None:
|
|
163
|
+
"""Provide ``cls``, built by the container from its constructor.
|
|
164
|
+
|
|
165
|
+
Does nothing when a definition for ``cls`` itself already exists — as
|
|
166
|
+
when the application marked it ``@injectable`` and a bundle's
|
|
167
|
+
autoconfiguration registers it again.
|
|
168
|
+
"""
|
|
169
|
+
self._allow("service", "load", "autoconfigure", "process")
|
|
170
|
+
if self._state.store.has_provider(cls):
|
|
171
|
+
return
|
|
172
|
+
key = (as_type if as_type is not None else cls, qualifier)
|
|
173
|
+
self._add(key, cls, "class", lifetime, priority)
|
|
174
|
+
|
|
175
|
+
def scan(self, *resources: str | ModuleType) -> None:
|
|
176
|
+
"""Scan ``resources`` too, once every bundle has loaded, as this bundle's.
|
|
177
|
+
|
|
178
|
+
This is how a bundle scans a module only when a peer is active.
|
|
179
|
+
"""
|
|
180
|
+
self._allow("scan", "load")
|
|
181
|
+
self._state.late_scans.append((self._origin.name, resources))
|
|
182
|
+
|
|
183
|
+
def autoconfigure(self, reader: Reader, apply: Apply) -> None:
|
|
184
|
+
"""Call ``apply`` for every metadata item ``reader`` finds on a scanned candidate."""
|
|
185
|
+
self._allow("autoconfigure", "load")
|
|
186
|
+
self._state.autoconfigurators.append(Autoconfigurator(self._origin.name, reader, apply))
|
|
187
|
+
|
|
188
|
+
def parameters(self, values: Mapping[str, object], /) -> None:
|
|
189
|
+
"""Merge ``values`` into the parameters, read with ``Inject(config=...)``."""
|
|
190
|
+
self._allow("parameters", "load", "autoconfigure", "process")
|
|
191
|
+
self._state.parameters.append((str(self._origin), values))
|
|
192
|
+
|
|
193
|
+
def resettable(
|
|
194
|
+
self, as_type: type, /, *, qualifier: Hashable | None = None, method: str = "reset"
|
|
195
|
+
) -> None:
|
|
196
|
+
"""Have ``ServicesResetter`` call ``method`` on the service, once it is built.
|
|
197
|
+
|
|
198
|
+
Resolved after every definition is final, so the order relative to
|
|
199
|
+
the service's own definition does not matter.
|
|
200
|
+
"""
|
|
201
|
+
self._allow("resettable", "load", "autoconfigure", "process")
|
|
202
|
+
self._state.resettables.append(ResettableRequest((as_type, qualifier), method))
|
|
203
|
+
|
|
204
|
+
def _add(
|
|
205
|
+
self, key: ServiceKey, provider: object, kind: Kind, lifetime: Lifetime, priority: int
|
|
206
|
+
) -> None:
|
|
207
|
+
definition = Definition(key, provider, kind, lifetime, self._origin, priority)
|
|
208
|
+
self._state.store.add(definition)
|
|
209
|
+
|
|
210
|
+
def _allow(self, operation: str, *phases: Phase) -> None:
|
|
211
|
+
phase = self._state.phase
|
|
212
|
+
if phase == "frozen":
|
|
213
|
+
raise BuilderFrozenError(operation)
|
|
214
|
+
if phase not in phases:
|
|
215
|
+
raise BuilderPhaseError(operation, phase)
|
|
216
|
+
|
|
217
|
+
def _require(self, key: ServiceKey, operation: str) -> Definition:
|
|
218
|
+
definition = self._state.store.get(key)
|
|
219
|
+
if definition is None:
|
|
220
|
+
raise UnknownServiceError(key, operation)
|
|
221
|
+
return definition
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def kind_of(provider: object) -> Kind:
|
|
225
|
+
"""Return how ``provider`` is registered, judged from what it is."""
|
|
226
|
+
if declaration_of(provider) is not None:
|
|
227
|
+
return "declared"
|
|
228
|
+
if isinstance(provider, type):
|
|
229
|
+
return "class"
|
|
230
|
+
if inspect.isfunction(provider):
|
|
231
|
+
return "factory"
|
|
232
|
+
return "instance"
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"""Declaring bundles: the base class, its decorator, and what the decorator records."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from .as_bundle import as_bundle
|
|
6
|
+
from .bundle import Bundle, NoConfig
|
|
7
|
+
from .bundle_metadata import BundleMetadata
|
|
8
|
+
|
|
9
|
+
__all__ = ["Bundle", "BundleMetadata", "NoConfig", "as_bundle"]
|