static-dependency-injector 0.2.0__tar.gz

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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Viacheslav Dermichev
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,130 @@
1
+ Metadata-Version: 2.4
2
+ Name: static-dependency-injector
3
+ Version: 0.2.0
4
+ Summary: Static, lazily-resolved declarative DI containers built on dependency-injector.
5
+ Keywords: dependency-injection,di,ioc,container,static,dependency-injector
6
+ Author: Viacheslav Dermichev
7
+ License-Expression: MIT
8
+ License-File: LICENSE
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: Programming Language :: Python :: 3 :: Only
12
+ Classifier: Programming Language :: Python :: 3.12
13
+ Classifier: Programming Language :: Python :: 3.13
14
+ Classifier: Programming Language :: Python :: 3.14
15
+ Classifier: Typing :: Typed
16
+ Classifier: Framework :: Pytest
17
+ Requires-Dist: dependency-injector>=4.49.1
18
+ Requires-Dist: ty>=0.0.55
19
+ Requires-Python: >=3.12
20
+ Project-URL: Homepage, https://github.com/v-dermichev/static-dependency-injector
21
+ Project-URL: Repository, https://github.com/v-dermichev/static-dependency-injector
22
+ Project-URL: Issues, https://github.com/v-dermichev/static-dependency-injector/issues
23
+ Description-Content-Type: text/markdown
24
+
25
+ # static-dependency-injector
26
+
27
+ [![PyPI](https://img.shields.io/pypi/v/static-dependency-injector)](https://pypi.org/project/static-dependency-injector/)
28
+ [![License](https://img.shields.io/pypi/l/static-dependency-injector)](https://github.com/v-dermichev/static-dependency-injector/blob/main/LICENSE)
29
+ [![GitHub](https://img.shields.io/badge/GitHub-v--dermichev%2Fstatic--dependency--injector-blue?logo=github)](https://github.com/v-dermichev/static-dependency-injector)
30
+
31
+ Static, lazily-resolved declarative DI containers built on top of
32
+ [`dependency-injector`](https://github.com/ets-labs/python-dependency-injector).
33
+
34
+ Declare providers as class attributes and read them back as resolved **values**
35
+ straight from the container class — no instance, no `()` call:
36
+
37
+ ```python
38
+ from static_dependency_injector import static_providers as sp
39
+ from static_dependency_injector.containers import StaticDeclarativeContainer
40
+
41
+
42
+ class Services(StaticDeclarativeContainer):
43
+ config = sp.Singleton(load_config)
44
+ db = sp.Singleton(Database, config=config)
45
+
46
+
47
+ db = Services.db # resolved Database instance (lazy, cached)
48
+ Services.set_overrides(db=fake_db) # override (e.g. in a test)
49
+ Services.clear_overrides("db") # clear it (or clear_overrides() for all)
50
+ ```
51
+
52
+ Providers stay lazy: the underlying callables are not invoked until first access,
53
+ and each provider keeps `dependency-injector`'s caching semantics.
54
+
55
+ ## Installation
56
+
57
+ ```bash
58
+ pip install static-dependency-injector
59
+ # or
60
+ uv add static-dependency-injector
61
+ ```
62
+
63
+ ## Why
64
+
65
+ - **Read providers as values** — `Services.db` returns the resolved instance, no
66
+ `()` call and no container instance.
67
+ - **Fully typed** — reads resolve to the dependency's type, and the override API
68
+ is clean under ty, mypy and pyright (Pylance).
69
+ - **Lazy & cached** — keeps `dependency-injector`'s resolution and per-provider
70
+ caching semantics; nothing is built until first access.
71
+ - **Test-friendly** — `set_overrides` / `clear_overrides` for per-test overrides,
72
+ plus `TestContextSingleton` auto-reset via a bundled pytest plugin.
73
+ - **Thin** — a typed wrapper over `dependency-injector`; no vendoring, no magic
74
+ beyond the container metaclass.
75
+
76
+ ## Providers
77
+
78
+ Typed wrappers over `dependency-injector` providers (`static_providers`):
79
+
80
+ `Factory`, `Singleton`, `ThreadSafeSingleton`, `ThreadLocalSingleton`,
81
+ `ContextLocalSingleton`, `Callable`, `Coroutine`, `Object`, `Resource`,
82
+ `Dependency`, `Selector`, `Provider`, and the test-scoped `TestContextSingleton`.
83
+
84
+ Each is generic in its resolved type, so `Services.db` reads back as the
85
+ dependency's type.
86
+
87
+ ## Test-scoped providers
88
+
89
+ `TestContextSingleton` is reset automatically after every test by the bundled
90
+ pytest plugin (auto-registered on install) — no manual `reset()` in teardown:
91
+
92
+ ```python
93
+ class Services(StaticDeclarativeContainer):
94
+ driver = sp.TestContextSingleton(make_driver)
95
+
96
+ # after each test the plugin calls Services.reset_test_context() for you
97
+ ```
98
+
99
+ You can also reset manually: `Services.reset_test_context()`.
100
+
101
+ ## Type checking
102
+
103
+ Reads resolve to the dependency's type, and the override API type-checks clean
104
+ under **ty**, **mypy** and **pyright** (Pylance) — verified by a test matrix that
105
+ runs all three. Overriding is done with `set_overrides` / `clear_overrides`;
106
+ direct attribute assignment (`Services.db = …`) is intentionally rejected by all
107
+ three checkers, so there is one obvious, typed way to override.
108
+
109
+ ## Notes & limitations
110
+
111
+ Because containers are resolved at the **class level** (no container instance):
112
+
113
+ - **Overrides** go through `set_overrides` / `clear_overrides`.
114
+ dependency_injector's *container-level* override
115
+ (`Container.override(other_container)`) does **not** affect resolution here -
116
+ reads come from the static registry, not a container instance.
117
+ - **`Resource` teardown** is not triggered automatically: a `Resource` provider
118
+ initializes on first access, but its post-`yield` teardown needs a container
119
+ instance / `shutdown_resources()`, which the class-level model does not drive.
120
+ Use `Resource` for init-only setup, or manage teardown yourself.
121
+
122
+ ## Requirements
123
+
124
+ - Python 3.12+ (uses PEP 695 generics)
125
+ - `dependency-injector >= 4.49.1`
126
+
127
+ ## License
128
+
129
+ MIT. Uses `dependency-injector` (BSD-3-Clause) as a runtime dependency; none of
130
+ its source is vendored.
@@ -0,0 +1,106 @@
1
+ # static-dependency-injector
2
+
3
+ [![PyPI](https://img.shields.io/pypi/v/static-dependency-injector)](https://pypi.org/project/static-dependency-injector/)
4
+ [![License](https://img.shields.io/pypi/l/static-dependency-injector)](https://github.com/v-dermichev/static-dependency-injector/blob/main/LICENSE)
5
+ [![GitHub](https://img.shields.io/badge/GitHub-v--dermichev%2Fstatic--dependency--injector-blue?logo=github)](https://github.com/v-dermichev/static-dependency-injector)
6
+
7
+ Static, lazily-resolved declarative DI containers built on top of
8
+ [`dependency-injector`](https://github.com/ets-labs/python-dependency-injector).
9
+
10
+ Declare providers as class attributes and read them back as resolved **values**
11
+ straight from the container class — no instance, no `()` call:
12
+
13
+ ```python
14
+ from static_dependency_injector import static_providers as sp
15
+ from static_dependency_injector.containers import StaticDeclarativeContainer
16
+
17
+
18
+ class Services(StaticDeclarativeContainer):
19
+ config = sp.Singleton(load_config)
20
+ db = sp.Singleton(Database, config=config)
21
+
22
+
23
+ db = Services.db # resolved Database instance (lazy, cached)
24
+ Services.set_overrides(db=fake_db) # override (e.g. in a test)
25
+ Services.clear_overrides("db") # clear it (or clear_overrides() for all)
26
+ ```
27
+
28
+ Providers stay lazy: the underlying callables are not invoked until first access,
29
+ and each provider keeps `dependency-injector`'s caching semantics.
30
+
31
+ ## Installation
32
+
33
+ ```bash
34
+ pip install static-dependency-injector
35
+ # or
36
+ uv add static-dependency-injector
37
+ ```
38
+
39
+ ## Why
40
+
41
+ - **Read providers as values** — `Services.db` returns the resolved instance, no
42
+ `()` call and no container instance.
43
+ - **Fully typed** — reads resolve to the dependency's type, and the override API
44
+ is clean under ty, mypy and pyright (Pylance).
45
+ - **Lazy & cached** — keeps `dependency-injector`'s resolution and per-provider
46
+ caching semantics; nothing is built until first access.
47
+ - **Test-friendly** — `set_overrides` / `clear_overrides` for per-test overrides,
48
+ plus `TestContextSingleton` auto-reset via a bundled pytest plugin.
49
+ - **Thin** — a typed wrapper over `dependency-injector`; no vendoring, no magic
50
+ beyond the container metaclass.
51
+
52
+ ## Providers
53
+
54
+ Typed wrappers over `dependency-injector` providers (`static_providers`):
55
+
56
+ `Factory`, `Singleton`, `ThreadSafeSingleton`, `ThreadLocalSingleton`,
57
+ `ContextLocalSingleton`, `Callable`, `Coroutine`, `Object`, `Resource`,
58
+ `Dependency`, `Selector`, `Provider`, and the test-scoped `TestContextSingleton`.
59
+
60
+ Each is generic in its resolved type, so `Services.db` reads back as the
61
+ dependency's type.
62
+
63
+ ## Test-scoped providers
64
+
65
+ `TestContextSingleton` is reset automatically after every test by the bundled
66
+ pytest plugin (auto-registered on install) — no manual `reset()` in teardown:
67
+
68
+ ```python
69
+ class Services(StaticDeclarativeContainer):
70
+ driver = sp.TestContextSingleton(make_driver)
71
+
72
+ # after each test the plugin calls Services.reset_test_context() for you
73
+ ```
74
+
75
+ You can also reset manually: `Services.reset_test_context()`.
76
+
77
+ ## Type checking
78
+
79
+ Reads resolve to the dependency's type, and the override API type-checks clean
80
+ under **ty**, **mypy** and **pyright** (Pylance) — verified by a test matrix that
81
+ runs all three. Overriding is done with `set_overrides` / `clear_overrides`;
82
+ direct attribute assignment (`Services.db = …`) is intentionally rejected by all
83
+ three checkers, so there is one obvious, typed way to override.
84
+
85
+ ## Notes & limitations
86
+
87
+ Because containers are resolved at the **class level** (no container instance):
88
+
89
+ - **Overrides** go through `set_overrides` / `clear_overrides`.
90
+ dependency_injector's *container-level* override
91
+ (`Container.override(other_container)`) does **not** affect resolution here -
92
+ reads come from the static registry, not a container instance.
93
+ - **`Resource` teardown** is not triggered automatically: a `Resource` provider
94
+ initializes on first access, but its post-`yield` teardown needs a container
95
+ instance / `shutdown_resources()`, which the class-level model does not drive.
96
+ Use `Resource` for init-only setup, or manage teardown yourself.
97
+
98
+ ## Requirements
99
+
100
+ - Python 3.12+ (uses PEP 695 generics)
101
+ - `dependency-injector >= 4.49.1`
102
+
103
+ ## License
104
+
105
+ MIT. Uses `dependency-injector` (BSD-3-Clause) as a runtime dependency; none of
106
+ its source is vendored.
@@ -0,0 +1,77 @@
1
+ [build-system]
2
+ requires = ["uv_build>=0.9,<0.12"]
3
+ build-backend = "uv_build"
4
+
5
+ [project]
6
+ name = "static-dependency-injector"
7
+ version = "0.2.0"
8
+ description = "Static, lazily-resolved declarative DI containers built on dependency-injector."
9
+ readme = "README.md"
10
+ requires-python = ">=3.12"
11
+ license = "MIT"
12
+ license-files = ["LICENSE"]
13
+ authors = [{ name = "Viacheslav Dermichev" }]
14
+ keywords = [
15
+ "dependency-injection",
16
+ "di",
17
+ "ioc",
18
+ "container",
19
+ "static",
20
+ "dependency-injector",
21
+ ]
22
+ classifiers = [
23
+ "Development Status :: 3 - Alpha",
24
+ "Intended Audience :: Developers",
25
+ "Programming Language :: Python :: 3 :: Only",
26
+ "Programming Language :: Python :: 3.12",
27
+ "Programming Language :: Python :: 3.13",
28
+ "Programming Language :: Python :: 3.14",
29
+ "Typing :: Typed",
30
+ "Framework :: Pytest",
31
+ ]
32
+ dependencies = [
33
+ "dependency-injector>=4.49.1",
34
+ "ty>=0.0.55",
35
+ ]
36
+
37
+ [project.urls]
38
+ Homepage = "https://github.com/v-dermichev/static-dependency-injector"
39
+ Repository = "https://github.com/v-dermichev/static-dependency-injector"
40
+ Issues = "https://github.com/v-dermichev/static-dependency-injector/issues"
41
+
42
+ # Auto-registered pytest plugin: resets TestContextSingleton providers per test.
43
+ [project.entry-points.pytest11]
44
+ static_dependency_injector = "static_dependency_injector._pytest_plugin"
45
+
46
+ [dependency-groups]
47
+ dev = [
48
+ "pytest>=8.0",
49
+ "ruff>=0.6",
50
+ "ty>=0.0.1a1",
51
+ "mypy>=1.13",
52
+ "pyright>=1.1.390",
53
+ ]
54
+
55
+ [tool.ty.terminal]
56
+ error-on-warning = true
57
+
58
+ [tool.ty.rules]
59
+ all = "error"
60
+
61
+ [tool.ruff]
62
+ line-length = 120
63
+ # Typing case files intentionally contain type errors; exclude from lint gate.
64
+ exclude = [".*", "__pycache__", "tests/typing/cases"]
65
+
66
+ [tool.ruff.lint]
67
+ select = ["E", "W", "F", "I", "N"]
68
+
69
+ [tool.pytest.ini_options]
70
+ addopts = ["--import-mode=importlib"]
71
+ markers = [
72
+ "typing: type-checker behavior tests (run ty / mypy / pyright on case files)",
73
+ ]
74
+
75
+ # Typing case files intentionally contain type errors; exclude from the ty gate.
76
+ [tool.ty.src]
77
+ exclude = ["tests/typing/cases"]
@@ -0,0 +1,20 @@
1
+ """pytest plugin: reset ``TestContextSingleton`` providers after each test.
2
+
3
+ Auto-registered through the ``pytest11`` entry point, so any test suite that
4
+ installs this package gets per-test reset of test-scoped providers for free,
5
+ replacing manual ``provider.reset()`` calls in test teardown.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ from typing import Any
10
+
11
+
12
+ def pytest_runtest_teardown(item: Any) -> None:
13
+ # Lazy import keeps plugin load cheap and avoids import-order coupling;
14
+ # failures here must never fail the test run.
15
+ try:
16
+ from static_dependency_injector.containers import StaticDeclarativeContainer
17
+
18
+ StaticDeclarativeContainer.reset_test_context()
19
+ except Exception: # noqa: BLE001
20
+ pass
@@ -0,0 +1,4 @@
1
+ from static_dependency_injector.containers._container_config_dict import ContainerConfigDict
2
+ from static_dependency_injector.containers._static_declarative_container import StaticDeclarativeContainer
3
+
4
+ __all__ = ("ContainerConfigDict", "StaticDeclarativeContainer")
@@ -0,0 +1,13 @@
1
+ from typing import TypedDict
2
+
3
+
4
+ class ContainerConfigDict(TypedDict, total=False):
5
+ """Per-container configuration.
6
+
7
+ ``name`` defaults to the fully-qualified class name (``module.QualName``).
8
+ Two container classes sharing a ``name`` share one set of provider
9
+ bindings (first writer under a name wins; overrides are visible to every
10
+ class registered under that name).
11
+ """
12
+
13
+ name: str
@@ -0,0 +1,57 @@
1
+ from typing import Any
2
+
3
+ from dependency_injector import containers
4
+
5
+ from static_dependency_injector.containers._static_declarative_container_meta import (
6
+ StaticDeclarativeContainerMeta,
7
+ _ResolvedProperty,
8
+ )
9
+
10
+
11
+ class StaticDeclarativeContainer(
12
+ containers.DeclarativeContainer,
13
+ metaclass=StaticDeclarativeContainerMeta,
14
+ ):
15
+ """Base class for static, lazily-resolved DI containers.
16
+
17
+ Subclass it and declare providers as class attributes; read them back as
18
+ resolved values via the class (``MyServices.db``). Replace providers per
19
+ test with :meth:`set_overrides` and restore them with :meth:`clear_overrides`.
20
+ """
21
+
22
+ @classmethod
23
+ def _provider_names(cls) -> frozenset[str]:
24
+ """Names of this container's providers (own and inherited)."""
25
+ return frozenset(
26
+ attr
27
+ for klass in type(cls).__mro__
28
+ for attr, value in vars(klass).items()
29
+ if isinstance(value, _ResolvedProperty)
30
+ )
31
+
32
+ @classmethod
33
+ def set_overrides(cls, **values: Any) -> None:
34
+ """Override named providers with concrete values until cleared.
35
+
36
+ ``MyServices.set_overrides(db=fake_db, clock=frozen)`` swaps each
37
+ provider's resolved value (running its ``on_set`` hook). A subsequent
38
+ ``MyServices.db`` then returns ``fake_db``. Prefer this over attribute
39
+ assignment - it type-checks cleanly.
40
+ """
41
+ unknown = set(values) - cls._provider_names()
42
+ if unknown:
43
+ raise TypeError(f"{cls.__name__} has no provider(s): {sorted(unknown)}")
44
+ for name, value in values.items():
45
+ setattr(cls, name, value)
46
+
47
+ @classmethod
48
+ def clear_overrides(cls, *names: str) -> None:
49
+ """Clear provider overrides. With ``names`` clear those; with none,
50
+ clear every provider on this container."""
51
+ valid = cls._provider_names()
52
+ targets = frozenset(names) if names else valid
53
+ unknown = targets - valid
54
+ if unknown:
55
+ raise TypeError(f"{cls.__name__} has no provider(s): {sorted(unknown)}")
56
+ for name in targets:
57
+ delattr(cls, name)
@@ -0,0 +1,98 @@
1
+ from __future__ import annotations
2
+
3
+ import threading
4
+ from typing import TYPE_CHECKING, Any, ClassVar, cast
5
+
6
+ from dependency_injector import providers
7
+
8
+ from static_dependency_injector.containers._container_config_dict import ContainerConfigDict
9
+
10
+ if TYPE_CHECKING:
11
+ # The shipped stubs don't export the declarative metaclass; for the checker
12
+ # we only need to know it is a metaclass (a ``type``). At runtime we subclass
13
+ # the real one so containers behave like dependency_injector's.
14
+ _DeclarativeContainerMeta = type
15
+ else:
16
+ from dependency_injector.containers import (
17
+ DeclarativeContainerMetaClass as _DeclarativeContainerMeta,
18
+ )
19
+
20
+
21
+ class _ResolvedProperty(property):
22
+ """Marker ``property`` subclass: lets a container's provider attributes be
23
+ told apart from any other properties on the metaclass MRO (e.g. those of
24
+ ``dependency_injector``)."""
25
+
26
+
27
+ class StaticDeclarativeContainerMeta(_DeclarativeContainerMeta):
28
+ """Lifts declared providers onto a per-class metaclass as resolved
29
+ properties, and registers them under the container's ``name``.
30
+ """
31
+
32
+ _REGISTRY: ClassVar[dict[str, dict[str, providers.Provider[Any]]]] = {}
33
+ _REGISTRY_LOCK: ClassVar[threading.RLock] = threading.RLock()
34
+
35
+ @staticmethod
36
+ def _resolved_property(name: str, attr: str) -> _ResolvedProperty:
37
+ """Build a class property that resolves provider ``name``/``attr`` lazily."""
38
+ registry = StaticDeclarativeContainerMeta._REGISTRY
39
+
40
+ def fget(_cls: Any) -> Any:
41
+ return registry[name][attr]()
42
+
43
+ def fset(cls: Any, value: Any) -> None:
44
+ provider = registry[name][attr]
45
+ hook = getattr(provider, "_static_di_on_set", None)
46
+ if hook is not None:
47
+ value = hook(cls, value)
48
+ provider.override(providers.Object(value))
49
+
50
+ def fdel(_cls: Any) -> None:
51
+ registry[name][attr].reset_override()
52
+
53
+ return _ResolvedProperty(fget, fset, fdel)
54
+
55
+ def __new__(
56
+ mcs,
57
+ cls_name: str,
58
+ bases: tuple[type, ...],
59
+ namespace: dict[str, Any],
60
+ **kwargs: Any,
61
+ ) -> StaticDeclarativeContainerMeta:
62
+ own = {attr: value for attr, value in namespace.items() if isinstance(value, providers.Provider)}
63
+ if own:
64
+ config: ContainerConfigDict = namespace.get("container_config", {})
65
+ module = namespace.get("__module__", "")
66
+ qualname = namespace.get("__qualname__", cls_name)
67
+ name = config.get("name") or (f"{module}.{qualname}" if module else qualname)
68
+ with mcs._REGISTRY_LOCK:
69
+ bucket = mcs._REGISTRY.setdefault(name, {})
70
+ for attr, provider in own.items():
71
+ bucket.setdefault(attr, provider) # first writer under a name wins
72
+ props = {attr: mcs._resolved_property(name, attr) for attr in own}
73
+ mcs = cast(
74
+ "type[StaticDeclarativeContainerMeta]",
75
+ type(f"_{cls_name}Meta", (mcs,), props), # ty:ignore[unsupported-dynamic-base]
76
+ )
77
+ return super().__new__(mcs, cls_name, bases, namespace, **kwargs)
78
+
79
+ @classmethod
80
+ def reset_test_context(cls) -> None:
81
+ """Reset every test-scoped provider (``TestContextSingleton``) across all
82
+ registered containers, so the next test gets fresh instances.
83
+
84
+ The bundled pytest plugin calls this on each test teardown, so a manual
85
+ ``provider.reset()`` in a test ``finally`` is no longer needed. Also
86
+ callable directly: ``MyServices.reset_test_context()``.
87
+ """
88
+ with cls._REGISTRY_LOCK:
89
+ test_scoped = [
90
+ provider
91
+ for bucket in cls._REGISTRY.values()
92
+ for provider in bucket.values()
93
+ if getattr(provider, "_static_di_test_scoped", False)
94
+ ]
95
+ for provider in test_scoped:
96
+ reset = getattr(provider, "reset", None)
97
+ if callable(reset):
98
+ reset()
@@ -0,0 +1,32 @@
1
+ from static_dependency_injector.static_providers._container_providers import (
2
+ Callable,
3
+ ContextLocalSingleton,
4
+ Coroutine,
5
+ Dependency,
6
+ Factory,
7
+ Object,
8
+ Provider,
9
+ Resource,
10
+ Selector,
11
+ Singleton,
12
+ TestContextSingleton,
13
+ ThreadLocalSingleton,
14
+ ThreadSafeSingleton,
15
+ )
16
+
17
+ __all__ = [
18
+ "Callable",
19
+ "ContextLocalSingleton",
20
+ "Coroutine",
21
+ "Dependency",
22
+ "Factory",
23
+ "Object",
24
+ "Provider",
25
+ "Resource",
26
+ "Selector",
27
+ "Singleton",
28
+ "TestContextSingleton",
29
+ "ThreadLocalSingleton",
30
+ "ThreadSafeSingleton",
31
+ ]
32
+
@@ -0,0 +1,101 @@
1
+ from collections.abc import Callable as _Callable
2
+ from typing import Any
3
+
4
+ from dependency_injector import providers
5
+
6
+ OnSetHook = _Callable[[Any, Any], Any]
7
+
8
+
9
+ class _ContainerProvider[T](providers.Provider[T]):
10
+ """Descriptor mixin for static-container providers.
11
+
12
+ Reading a container attribute resolves the dependency (``__get__`` ->
13
+ ``self()``), typed as ``T``. Overriding is done through the container's
14
+ ``set_overrides`` / ``clear_overrides``; direct attribute assignment is
15
+ intentionally not part of the typed API. ``on_set`` registers a hook run
16
+ over the value before an override is applied.
17
+ """
18
+
19
+ _static_di_on_set: OnSetHook | None = None
20
+
21
+ def on_set(self, hook: OnSetHook) -> OnSetHook:
22
+ self._static_di_on_set = hook
23
+ return hook
24
+
25
+ def __get__(self, _: Any, __: Any = None) -> T:
26
+ return self()
27
+
28
+
29
+ # --- Instance factories --------------------------------------------------
30
+
31
+
32
+ class Factory[T](_ContainerProvider[T], providers.Factory[T]):
33
+ """``Factory``: builds a new instance on every access."""
34
+
35
+
36
+ class Singleton[T](_ContainerProvider[T], providers.Singleton[T]):
37
+ """``Singleton``: one instance per container, cached after the first access."""
38
+
39
+
40
+ class ThreadSafeSingleton[T](_ContainerProvider[T], providers.ThreadSafeSingleton[T]):
41
+ """``Singleton`` guarded by a lock while the instance is created."""
42
+
43
+
44
+ class ThreadLocalSingleton[T](_ContainerProvider[T], providers.ThreadLocalSingleton[T]):
45
+ """``Singleton`` with a separate instance per thread."""
46
+
47
+
48
+ class ContextLocalSingleton[T](_ContainerProvider[T], providers.ContextLocalSingleton[T]):
49
+ """``Singleton`` with a separate instance per ``contextvars`` context."""
50
+
51
+
52
+ # --- Calls ---------------------------------------------------------------
53
+
54
+
55
+ class Callable[T](_ContainerProvider[T], providers.Callable[T]):
56
+ """``Callable``: calls a function with the given arguments and returns the result."""
57
+
58
+
59
+ class Coroutine[T](_ContainerProvider[T], providers.Coroutine[T]):
60
+ """``async`` variant of ``Callable`` for coroutines; returns an awaitable."""
61
+
62
+
63
+ # --- Values, resources, dependencies -------------------------------------
64
+
65
+
66
+ class Object[T](_ContainerProvider[T], providers.Object[T]):
67
+ """``Object``: returns the given value unchanged."""
68
+
69
+
70
+ class Resource[T](_ContainerProvider[T], providers.Resource[T]):
71
+ """``Resource``: a resource with init/teardown (generator or context manager)."""
72
+
73
+
74
+ class Dependency[T](_ContainerProvider[T], providers.Dependency[T]):
75
+ """``Dependency``: declares a dependency that the caller must provide."""
76
+
77
+
78
+ class Selector[T](_ContainerProvider[T], providers.Selector[T]):
79
+ """``Selector``: picks a provider by a selector value at runtime."""
80
+
81
+
82
+ class Provider[T](_ContainerProvider[T], providers.Provider[T]):
83
+ """Base provider / escape hatch for custom implementations."""
84
+
85
+
86
+ # --- Test scope ----------------------------------------------------------
87
+
88
+
89
+ class TestContextSingleton[T](ContextLocalSingleton[T]):
90
+ """``ContextLocalSingleton`` that is reset at the test boundary.
91
+
92
+ Marked test-scoped: the bundled pytest plugin calls
93
+ ``StaticDeclarativeContainer.reset_test_context()`` after each test, which
94
+ resets such providers. A manual ``provider.reset()`` in a test ``finally``
95
+ is therefore unnecessary - the next test gets a fresh instance on its own
96
+ ``contextvars`` context.
97
+ """
98
+
99
+ __test__ = False # keep pytest from collecting this Test-prefixed class
100
+ _static_di_test_scoped: bool = True
101
+