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.
Files changed (78) hide show
  1. xtr_dependency_injection/__init__.py +72 -0
  2. xtr_dependency_injection/builder/__init__.py +7 -0
  3. xtr_dependency_injection/builder/autoconfigurator.py +71 -0
  4. xtr_dependency_injection/builder/conflict_policy.py +94 -0
  5. xtr_dependency_injection/builder/container_builder.py +125 -0
  6. xtr_dependency_injection/builder/definition.py +67 -0
  7. xtr_dependency_injection/builder/service_configurator.py +232 -0
  8. xtr_dependency_injection/bundle/__init__.py +9 -0
  9. xtr_dependency_injection/bundle/as_bundle.py +110 -0
  10. xtr_dependency_injection/bundle/bundle.py +93 -0
  11. xtr_dependency_injection/bundle/bundle_metadata.py +29 -0
  12. xtr_dependency_injection/compiler/__init__.py +9 -0
  13. xtr_dependency_injection/compiler/_wireup_bridge.py +114 -0
  14. xtr_dependency_injection/compiler/registration.py +347 -0
  15. xtr_dependency_injection/compiler/wireup_compiler.py +201 -0
  16. xtr_dependency_injection/config/__init__.py +9 -0
  17. xtr_dependency_injection/config/config_prepender.py +88 -0
  18. xtr_dependency_injection/config/config_provider.py +129 -0
  19. xtr_dependency_injection/config/config_resolver.py +235 -0
  20. xtr_dependency_injection/config/configure.py +61 -0
  21. xtr_dependency_injection/config/env.py +80 -0
  22. xtr_dependency_injection/config/parameters.py +99 -0
  23. xtr_dependency_injection/decorator/__init__.py +21 -0
  24. xtr_dependency_injection/decorator/_marker.py +27 -0
  25. xtr_dependency_injection/decorator/as_decorator.py +116 -0
  26. xtr_dependency_injection/decorator/compiler_pass.py +45 -0
  27. xtr_dependency_injection/decorator/exclude.py +23 -0
  28. xtr_dependency_injection/decorator/lifecycle.py +64 -0
  29. xtr_dependency_injection/decorator/when.py +65 -0
  30. xtr_dependency_injection/diagnostics/__init__.py +7 -0
  31. xtr_dependency_injection/diagnostics/report.py +217 -0
  32. xtr_dependency_injection/discovery/__init__.py +7 -0
  33. xtr_dependency_injection/discovery/bundle_discovery.py +84 -0
  34. xtr_dependency_injection/discovery/bundle_resolver.py +302 -0
  35. xtr_dependency_injection/exception/__init__.py +51 -0
  36. xtr_dependency_injection/exception/_naming.py +32 -0
  37. xtr_dependency_injection/exception/builder_frozen_error.py +18 -0
  38. xtr_dependency_injection/exception/builder_phase_error.py +24 -0
  39. xtr_dependency_injection/exception/bundle_definition_error.py +25 -0
  40. xtr_dependency_injection/exception/circular_bundle_dependency_error.py +18 -0
  41. xtr_dependency_injection/exception/config_provider_error.py +25 -0
  42. xtr_dependency_injection/exception/conflicting_config_providers_error.py +23 -0
  43. xtr_dependency_injection/exception/decorator_signature_error.py +20 -0
  44. xtr_dependency_injection/exception/dependency_injection_error.py +14 -0
  45. xtr_dependency_injection/exception/duplicate_bundle_error.py +26 -0
  46. xtr_dependency_injection/exception/duplicate_service_error.py +35 -0
  47. xtr_dependency_injection/exception/invalid_environment_error.py +20 -0
  48. xtr_dependency_injection/exception/invalid_environment_variable_error.py +28 -0
  49. xtr_dependency_injection/exception/kernel_already_booted_error.py +18 -0
  50. xtr_dependency_injection/exception/missing_bundle_error.py +24 -0
  51. xtr_dependency_injection/exception/missing_environment_variable_error.py +18 -0
  52. xtr_dependency_injection/exception/parameter_conflict_error.py +22 -0
  53. xtr_dependency_injection/exception/resource_import_error.py +18 -0
  54. xtr_dependency_injection/exception/unknown_config_type_error.py +32 -0
  55. xtr_dependency_injection/exception/unknown_locator_key_error.py +26 -0
  56. xtr_dependency_injection/exception/unknown_service_error.py +26 -0
  57. xtr_dependency_injection/kernel/__init__.py +10 -0
  58. xtr_dependency_injection/kernel/booted_kernel.py +106 -0
  59. xtr_dependency_injection/kernel/compiled_kernel.py +102 -0
  60. xtr_dependency_injection/kernel/kernel.py +563 -0
  61. xtr_dependency_injection/kernel/kernel_bundle.py +63 -0
  62. xtr_dependency_injection/kernel/kernel_interface.py +109 -0
  63. xtr_dependency_injection/py.typed +0 -0
  64. xtr_dependency_injection/runtime/__init__.py +9 -0
  65. xtr_dependency_injection/runtime/bind_callable.py +143 -0
  66. xtr_dependency_injection/runtime/service_locator.py +60 -0
  67. xtr_dependency_injection/runtime/services_resetter.py +63 -0
  68. xtr_dependency_injection/scan/__init__.py +7 -0
  69. xtr_dependency_injection/scan/default_excludes.py +21 -0
  70. xtr_dependency_injection/scan/scanned_object.py +26 -0
  71. xtr_dependency_injection/scan/scanner.py +192 -0
  72. xtr_dependency_injection/standalone.py +100 -0
  73. xtr_dependency_injection/testing/__init__.py +57 -0
  74. xtr_dependency_injection/testing/pytest_plugin.py +54 -0
  75. xtr_dependency_injection-1.0.0.dist-info/METADATA +469 -0
  76. xtr_dependency_injection-1.0.0.dist-info/RECORD +78 -0
  77. xtr_dependency_injection-1.0.0.dist-info/WHEEL +4 -0
  78. 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,7 @@
1
+ """Collecting service definitions: per bundle while loading, as a whole while processing."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from .definition import Definition, Lifetime, Origin, ServiceKey
6
+
7
+ __all__ = ["Definition", "Lifetime", "Origin", "ServiceKey"]
@@ -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"]