static-dependency-injector 0.2.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.
- static_dependency_injector/__init__.py +0 -0
- static_dependency_injector/_pytest_plugin.py +20 -0
- static_dependency_injector/containers/__init__.py +4 -0
- static_dependency_injector/containers/_container_config_dict.py +13 -0
- static_dependency_injector/containers/_static_declarative_container.py +57 -0
- static_dependency_injector/containers/_static_declarative_container_meta.py +98 -0
- static_dependency_injector/py.typed +0 -0
- static_dependency_injector/static_providers/__init__.py +32 -0
- static_dependency_injector/static_providers/_container_providers.py +101 -0
- static_dependency_injector-0.2.0.dist-info/METADATA +130 -0
- static_dependency_injector-0.2.0.dist-info/RECORD +14 -0
- static_dependency_injector-0.2.0.dist-info/WHEEL +4 -0
- static_dependency_injector-0.2.0.dist-info/entry_points.txt +3 -0
- static_dependency_injector-0.2.0.dist-info/licenses/LICENSE +21 -0
|
File without changes
|
|
@@ -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,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()
|
|
File without changes
|
|
@@ -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
|
+
|
|
@@ -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
|
+
[](https://pypi.org/project/static-dependency-injector/)
|
|
28
|
+
[](https://github.com/v-dermichev/static-dependency-injector/blob/main/LICENSE)
|
|
29
|
+
[](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,14 @@
|
|
|
1
|
+
static_dependency_injector/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
static_dependency_injector/_pytest_plugin.py,sha256=aUCicAf7EszZM_RJKV0PqNa-thRkUdSiabf68cUJAxw,734
|
|
3
|
+
static_dependency_injector/containers/__init__.py,sha256=BAIVwzxPfWut2dmla3NVl2Q8suiexezMU4voRf8ytFU,265
|
|
4
|
+
static_dependency_injector/containers/_container_config_dict.py,sha256=5Al0TyA7gg4IypuRxHqCWC4YDYkbHV_xJSZls3u6joQ,408
|
|
5
|
+
static_dependency_injector/containers/_static_declarative_container.py,sha256=08-iOkpALKdy5j4ShT6Dehz-0MDviRBAcKRpeB6az4E,2139
|
|
6
|
+
static_dependency_injector/containers/_static_declarative_container_meta.py,sha256=ckkP_EF-Z4ZTZErVHYzIs4zkrJH5S41l1Z0sf5Mox1U,4017
|
|
7
|
+
static_dependency_injector/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
8
|
+
static_dependency_injector/static_providers/__init__.py,sha256=sqemBVmxVJkn34-98uxyLM-oCv1_8XI3K2lgx1Ga9VQ,585
|
|
9
|
+
static_dependency_injector/static_providers/_container_providers.py,sha256=Oumjs1w3oIX7meNbjv_qMtWc4OF8s0MKaZwLonosxD4,3520
|
|
10
|
+
static_dependency_injector-0.2.0.dist-info/licenses/LICENSE,sha256=RUqQucXRgEKTNKv-JXOsd-LIwDAIEaeFL4GaugvxWis,1077
|
|
11
|
+
static_dependency_injector-0.2.0.dist-info/WHEEL,sha256=s49dN1sxqzkgPplo4QuUaKomil-_cbDzeLK4-pZKD-A,81
|
|
12
|
+
static_dependency_injector-0.2.0.dist-info/entry_points.txt,sha256=LnBlqd_n4r8XThXjONEK29Lf6oLVovBx5oD6qM0e8lk,83
|
|
13
|
+
static_dependency_injector-0.2.0.dist-info/METADATA,sha256=DionKJacd_pyTFdotsstWEPU6fVjgP6gKGpgO3ctMaA,5388
|
|
14
|
+
static_dependency_injector-0.2.0.dist-info/RECORD,,
|
|
@@ -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.
|