diff --git a/Justfile b/Justfile index 228c708..26d87ff 100644 --- a/Justfile +++ b/Justfile @@ -3,6 +3,7 @@ default: install lint test install: uv lock --upgrade uv sync --only-dev --frozen + uv run pre-commit install --overwrite lint: uv run ruff format diff --git a/docs/migration/v2.md b/docs/migration/v2.md index b90976a..70f35a9 100644 --- a/docs/migration/v2.md +++ b/docs/migration/v2.md @@ -147,4 +147,3 @@ To resolve such issues in `2.*`, consider the following suggestions: ## Further Help If you continue to experience issues during migration, consider creating a [discussion](https://github.com/modern-python/that-depends/discussions) or opening an [issue](https://github.com/modern-python/that-depends/issues). -``` diff --git a/pyproject.toml b/pyproject.toml index 370ebaa..5fc205e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -65,7 +65,8 @@ extend-exclude = [ [tool.ruff.lint] select = ["ALL"] ignore = [ - "D1", # allow missing docstrings + "D100", # ignore missing module docstrings. + "D105", # ignore missing docstrings in magic methods. "S101", # allow asserts "TCH", # ignore flake8-type-checking "FBT", # allow boolean args @@ -76,6 +77,7 @@ ignore = [ ] isort.lines-after-imports = 2 isort.no-lines-before = ["standard-library", "local-folder"] +per-file-ignores = { "tests/*"= ["D1", "SLF001"]} [tool.pytest.ini_options] addopts = "--cov=. --cov-report term-missing" diff --git a/tests/integrations/fastapi/test_fastapi_di.py b/tests/integrations/fastapi/test_fastapi_di.py index 08c7896..ca46e8b 100644 --- a/tests/integrations/fastapi/test_fastapi_di.py +++ b/tests/integrations/fastapi/test_fastapi_di.py @@ -18,12 +18,11 @@ def fastapi_app(request: pytest.FixtureRequest) -> fastapi.FastAPI: app = fastapi.FastAPI() if request.param: - app.add_middleware(DIContextMiddleware, request.param, global_context=_GLOBAL_CONTEXT) - else: app.add_middleware( - DIContextMiddleware, - global_context=_GLOBAL_CONTEXT, + DIContextMiddleware, request.param, global_context=_GLOBAL_CONTEXT, reset_all_containers=True ) + else: + app.add_middleware(DIContextMiddleware, global_context=_GLOBAL_CONTEXT, reset_all_containers=True) @app.get("/") async def read_root( diff --git a/tests/providers/test_context_resources.py b/tests/providers/test_context_resources.py index c17241e..e7ef285 100644 --- a/tests/providers/test_context_resources.py +++ b/tests/providers/test_context_resources.py @@ -1,6 +1,8 @@ import asyncio import datetime import logging +import threading +import time import typing import uuid from contextlib import AsyncExitStack, ExitStack @@ -208,7 +210,7 @@ async def test_async_injection_when_resetting_resource_specific_context( @async_context_resource.context @inject async def _async_injected(val: str = Provide[async_context_resource]) -> str: - assert isinstance(async_context_resource._fetch_context().context_stack, AsyncExitStack) # noqa: SLF001 + assert isinstance(async_context_resource._fetch_context().context_stack, AsyncExitStack) return val async_result = await _async_injected() @@ -224,13 +226,13 @@ async def test_sync_injection_when_resetting_resource_specific_context( @sync_context_resource.context @inject async def _async_injected(val: str = Provide[sync_context_resource]) -> str: - assert isinstance(sync_context_resource._fetch_context().context_stack, ExitStack) # noqa: SLF001 + assert isinstance(sync_context_resource._fetch_context().context_stack, ExitStack) return val @sync_context_resource.context @inject def _sync_injected(val: str = Provide[sync_context_resource]) -> str: - assert isinstance(sync_context_resource._fetch_context().context_stack, ExitStack) # noqa: SLF001 + assert isinstance(sync_context_resource._fetch_context().context_stack, ExitStack) return val async_result = await _async_injected() @@ -290,7 +292,7 @@ async def test_async_injection_when_explicitly_resetting_resource_specific_conte @async_context_resource.async_context() @inject async def _async_injected(val: str = Provide[async_context_resource]) -> str: - assert isinstance(async_context_resource._fetch_context().context_stack, AsyncExitStack) # noqa: SLF001 + assert isinstance(async_context_resource._fetch_context().context_stack, AsyncExitStack) return val async_result = await _async_injected() @@ -306,13 +308,13 @@ async def test_sync_injection_when_explicitly_resetting_resource_specific_contex @sync_context_resource.async_context() @inject async def _async_injected(val: str = Provide[sync_context_resource]) -> str: - assert isinstance(sync_context_resource._fetch_context().context_stack, ExitStack) # noqa: SLF001 + assert isinstance(sync_context_resource._fetch_context().context_stack, ExitStack) return val @sync_context_resource.sync_context() @inject def _sync_injected(val: str = Provide[sync_context_resource]) -> str: - assert isinstance(sync_context_resource._fetch_context().context_stack, ExitStack) # noqa: SLF001 + assert isinstance(sync_context_resource._fetch_context().context_stack, ExitStack) return val async_result = await _async_injected() @@ -570,19 +572,19 @@ def test_enter_sync_context_for_async_resource_should_throw( async_context_resource: providers.ContextResource[str], ) -> None: with pytest.raises(RuntimeError): - async_context_resource.__enter__() + async_context_resource._enter_sync_context() def test_exit_sync_context_before_enter_should_throw(sync_context_resource: providers.ContextResource[str]) -> None: with pytest.raises(RuntimeError): - sync_context_resource.__exit__(None, None, None) + sync_context_resource._exit_sync_context() async def test_exit_async_context_before_enter_should_throw( async_context_resource: providers.ContextResource[str], ) -> None: with pytest.raises(RuntimeError): - await async_context_resource.__aexit__(None, None, None) + await async_context_resource._exit_async_context() def test_enter_sync_context_from_async_resource_should_throw( @@ -608,3 +610,39 @@ async def test_preserve_globals_and_initial_context() -> None: assert fetch_context_item(key) == item for key in new_context: assert fetch_context_item(key) is None + + +async def test_async_context_switching_with_asyncio() -> None: + async def slow_async_creator() -> typing.AsyncIterator[str]: + await asyncio.sleep(0.1) + yield str(uuid.uuid4()) + + class MyContainer(BaseContainer): + slow_provider = providers.ContextResource(slow_async_creator) + + async def _injected() -> str: + async with MyContainer.slow_provider.async_context(): + return await MyContainer.slow_provider.async_resolve() + + await asyncio.gather(*[_injected() for _ in range(10)]) + + +def test_sync_context_switching_with_threads() -> None: + def slow_sync_creator() -> typing.Iterator[str]: + time.sleep(0.1) + yield str(uuid.uuid4()) + + class MyContainer(BaseContainer): + slow_provider = providers.ContextResource(slow_sync_creator) + + def _injected() -> str: + with MyContainer.slow_provider.sync_context(): + return MyContainer.slow_provider.sync_resolve() + + threads = [threading.Thread(target=_injected) for _ in range(10)] + + for thread in threads: + thread.start() + + for thread in threads: + thread.join() diff --git a/tests/test_multiple_containers.py b/tests/test_multiple_containers.py index fbfeadf..e1dff2e 100644 --- a/tests/test_multiple_containers.py +++ b/tests/test_multiple_containers.py @@ -21,14 +21,14 @@ async def test_included_container() -> None: assert all(isinstance(x, datetime.datetime) for x in sequence) await OuterContainer.tear_down() - assert InnerContainer.sync_resource._context.instance is None # noqa: SLF001 - assert InnerContainer.async_resource._context.instance is None # noqa: SLF001 + assert InnerContainer.sync_resource._context.instance is None + assert InnerContainer.async_resource._context.instance is None await OuterContainer.init_resources() - sync_resource_context = InnerContainer.sync_resource._context # noqa: SLF001 + sync_resource_context = InnerContainer.sync_resource._context assert sync_resource_context assert sync_resource_context.instance is not None - async_resource_context = InnerContainer.async_resource._context # noqa: SLF001 + async_resource_context = InnerContainer.async_resource._context assert async_resource_context assert async_resource_context.instance is not None await OuterContainer.tear_down() diff --git a/that_depends/__init__.py b/that_depends/__init__.py index ac12ce2..aac2a91 100644 --- a/that_depends/__init__.py +++ b/that_depends/__init__.py @@ -1,3 +1,5 @@ +"""Dependency injection framework for Python.""" + from that_depends import providers from that_depends.container import BaseContainer from that_depends.injection import Provide, inject diff --git a/that_depends/container.py b/that_depends/container.py index 7fe71c6..7f102f6 100644 --- a/that_depends/container.py +++ b/that_depends/container.py @@ -2,6 +2,8 @@ import typing from contextlib import AsyncExitStack, ExitStack, asynccontextmanager, contextmanager +from typing_extensions import override + from that_depends.meta import BaseContainerMeta from that_depends.providers import AbstractProvider, Resource, Singleton from that_depends.providers.context_resources import ContextResource, SupportsContext @@ -16,19 +18,24 @@ class BaseContainer(SupportsContext[None], metaclass=BaseContainerMeta): + """Base container class.""" + providers: dict[str, AbstractProvider[typing.Any]] containers: list[type["BaseContainer"]] - def __new__(cls, *_: typing.Any, **__: typing.Any) -> "typing_extensions.Self": # noqa: ANN401 + @override + def __new__(cls, *_: typing.Any, **__: typing.Any) -> "typing_extensions.Self": msg = f"{cls.__name__} should not be instantiated" raise RuntimeError(msg) @classmethod + @override def supports_sync_context(cls) -> bool: return True @classmethod @contextmanager + @override def sync_context(cls) -> typing.Iterator[None]: with ExitStack() as stack: for container in cls.get_containers(): @@ -40,6 +47,7 @@ def sync_context(cls) -> typing.Iterator[None]: @classmethod @asynccontextmanager + @override async def async_context(cls) -> typing.AsyncIterator[None]: async with AsyncExitStack() as stack: for container in cls.get_containers(): @@ -50,6 +58,7 @@ async def async_context(cls) -> typing.AsyncIterator[None]: yield @classmethod + @override def context(cls, func: typing.Callable[P, T]) -> typing.Callable[P, T]: if inspect.iscoroutinefunction(func): @@ -79,12 +88,14 @@ def connect_containers(cls, *containers: type["BaseContainer"]) -> None: @classmethod def get_providers(cls) -> dict[str, AbstractProvider[typing.Any]]: + """Get all connected providers.""" if not hasattr(cls, "providers"): cls.providers = {k: v for k, v in cls.__dict__.items() if isinstance(v, AbstractProvider)} return cls.providers @classmethod def get_containers(cls) -> list[type["BaseContainer"]]: + """Get all connected containers.""" if not hasattr(cls, "containers"): cls.containers = [] @@ -92,6 +103,7 @@ def get_containers(cls) -> list[type["BaseContainer"]]: @classmethod async def init_resources(cls) -> None: + """Initialize all resources.""" for provider in cls.get_providers().values(): if isinstance(provider, Resource): await provider.async_resolve() @@ -101,6 +113,7 @@ async def init_resources(cls) -> None: @classmethod async def tear_down(cls) -> None: + """Tear down all resources.""" for provider in reversed(cls.get_providers().values()): if isinstance(provider, Resource | Singleton): await provider.tear_down() @@ -110,11 +123,22 @@ async def tear_down(cls) -> None: @classmethod def reset_override(cls) -> None: + """Reset all provider overrides.""" for v in cls.get_providers().values(): v.reset_override() @classmethod def resolver(cls, item: typing.Callable[P, T]) -> typing.Callable[[], typing.Awaitable[T]]: + """Decorate a function to automatically resolve dependencies on call by name. + + Args: + item: objects for which the dependencies should be resolved. + + Returns: + Async wrapped callable with auto-injected dependencies. + + """ + async def _inner() -> T: return await cls.resolve(item) @@ -122,6 +146,7 @@ async def _inner() -> T: @classmethod async def resolve(cls, object_to_resolve: typing.Callable[..., T]) -> T: + """Inject dependencies into an object automatically by name.""" signature: typing.Final = inspect.signature(object_to_resolve) kwargs = {} providers: typing.Final = cls.get_providers() @@ -140,6 +165,15 @@ async def resolve(cls, object_to_resolve: typing.Callable[..., T]) -> T: @classmethod @contextmanager def override_providers(cls, providers_for_overriding: dict[str, typing.Any]) -> typing.Iterator[None]: + """Override several providers with mocks simultaneously. + + Args: + providers_for_overriding: {provider_name: mock} dictionary. + + Returns: + None + + """ current_providers: typing.Final = cls.get_providers() current_provider_names: typing.Final = set(current_providers.keys()) given_provider_names: typing.Final = set(providers_for_overriding.keys()) diff --git a/that_depends/entities/__init__.py b/that_depends/entities/__init__.py index e69de29..ff09aa5 100644 --- a/that_depends/entities/__init__.py +++ b/that_depends/entities/__init__.py @@ -0,0 +1 @@ +"""Entities.""" diff --git a/that_depends/entities/resource_context.py b/that_depends/entities/resource_context.py index b37a4f4..d3911a4 100644 --- a/that_depends/entities/resource_context.py +++ b/that_depends/entities/resource_context.py @@ -8,14 +8,18 @@ class ResourceContext(typing.Generic[T_co]): + """Class to manage a resources' context.""" + __slots__ = "asyncio_lock", "context_stack", "instance", "is_async", "threading_lock" def __init__(self, is_async: bool) -> None: """Create a new ResourceContext instance. - :param is_async: Whether the ResourceContext was created in an async context. + Args: + is_async (bool): Whether the ResourceContext was created in + an async context. For example within a ``async with container_context(): ...`` statement. - :type is_async: bool + """ self.instance: T_co | None = None self.asyncio_lock: typing.Final = asyncio.Lock() @@ -27,15 +31,18 @@ def __init__(self, is_async: bool) -> None: def is_context_stack_async( context_stack: contextlib.AsyncExitStack | contextlib.ExitStack | None, ) -> typing.TypeGuard[contextlib.AsyncExitStack]: + """Check if the context stack is an async context stack.""" return isinstance(context_stack, contextlib.AsyncExitStack) @staticmethod def is_context_stack_sync( context_stack: contextlib.AsyncExitStack | contextlib.ExitStack, ) -> typing.TypeGuard[contextlib.ExitStack]: + """Check if the context stack is a sync context stack.""" return isinstance(context_stack, contextlib.ExitStack) async def tear_down(self) -> None: + """Tear down the async context stack.""" if self.context_stack is None: return @@ -47,6 +54,7 @@ async def tear_down(self) -> None: self.instance = None def sync_tear_down(self) -> None: + """Tear down the sync context stack.""" if self.context_stack is None: return diff --git a/that_depends/injection.py b/that_depends/injection.py index b4661c4..12d4a26 100644 --- a/that_depends/injection.py +++ b/that_depends/injection.py @@ -13,6 +13,23 @@ def inject( func: typing.Callable[P, T], ) -> typing.Callable[P, T]: + """Decorate a function to enable dependency injection. + + Args: + func: sync or async function with dependencies. + + Returns: + function that will resolve dependencies on call. + + + Example: + ```python + @inject + async def func(a: str = Provide[Container.a_provider]) -> str: + ... + ``` + + """ if inspect.iscoroutinefunction(func): return typing.cast(typing.Callable[P, T], _inject_to_async(func)) @@ -77,8 +94,11 @@ def inner(*args: P.args, **kwargs: P.kwargs) -> T: class ClassGetItemMeta(type): + """Metaclass to support Provide[provider] syntax.""" + def __getitem__(cls, provider: AbstractProvider[T]) -> T: return typing.cast(T, provider) -class Provide(metaclass=ClassGetItemMeta): ... +class Provide(metaclass=ClassGetItemMeta): + """Marker to dependency injection.""" diff --git a/that_depends/meta.py b/that_depends/meta.py index 7ac08b8..0315a6d 100644 --- a/that_depends/meta.py +++ b/that_depends/meta.py @@ -2,16 +2,21 @@ import typing from threading import Lock +from typing_extensions import override + if typing.TYPE_CHECKING: from that_depends.container import BaseContainer class BaseContainerMeta(abc.ABCMeta): + """Metaclass for BaseContainer.""" + _instances: typing.ClassVar[list[type["BaseContainer"]]] = [] _lock: Lock = Lock() + @override def __new__(cls, name: str, bases: tuple[type, ...], namespace: dict[str, typing.Any]) -> type: new_cls = super().__new__(cls, name, bases, namespace) with cls._lock: @@ -21,4 +26,5 @@ def __new__(cls, name: str, bases: tuple[type, ...], namespace: dict[str, typing @classmethod def get_instances(cls) -> list[type["BaseContainer"]]: + """Get all instances that inherit from BaseContainer.""" return cls._instances diff --git a/that_depends/providers/__init__.py b/that_depends/providers/__init__.py index dd6ed8d..3bdf9e4 100644 --- a/that_depends/providers/__init__.py +++ b/that_depends/providers/__init__.py @@ -1,3 +1,5 @@ +"""Providers.""" + from that_depends.providers.base import AbstractProvider, AttrGetter from that_depends.providers.collection import Dict, List from that_depends.providers.context_resources import ( diff --git a/that_depends/providers/base.py b/that_depends/providers/base.py index 4c66d7c..f546acd 100644 --- a/that_depends/providers/base.py +++ b/that_depends/providers/base.py @@ -6,6 +6,7 @@ from operator import attrgetter import typing_extensions +from typing_extensions import override from that_depends.entities.resource_context import ResourceContext @@ -20,7 +21,10 @@ class AbstractProvider(typing.Generic[T_co], abc.ABC): + """Base class for all providers.""" + def __init__(self) -> None: + """Create a new provider.""" super().__init__() self._override: typing.Any = None @@ -32,6 +36,15 @@ def __deepcopy__(self, *_: object, **__: object) -> typing_extensions.Self: return self def __getattr__(self, attr_name: str) -> typing.Any: # noqa: ANN401 + """Get an attribute from the resolve object. + + Args: + attr_name: name of attribute to get. + + Returns: + An `AttrGetter` provider that will get the attribute after resolving the current provider. + + """ if attr_name.startswith("_"): msg = f"'{type(self)}' object has no attribute '{attr_name}'" raise AttributeError(msg) @@ -46,13 +59,32 @@ def sync_resolve(self) -> T_co: """Resolve dependency synchronously.""" async def __call__(self) -> T_co: + """Resolve dependency asynchronously.""" return await self.async_resolve() def override(self, mock: object) -> None: + """Override the provider with a mock object. + + Args: + mock: object to resolve while the provider is overridden. + + Returns: + None + + """ self._override = mock @contextmanager def override_context(self, mock: object) -> typing.Iterator[None]: + """Override the provider with a mock object temporarily. + + Args: + mock: object to resolve while the provider is overridden. + + Returns: + None + + """ self.override(mock) try: yield @@ -60,6 +92,11 @@ def override_context(self, mock: object) -> typing.Iterator[None]: self.reset_override() def reset_override(self) -> None: + """Reset the provider to its original state. + + Use this is you have previously called `override` or `override_context` + to reset the provider to its original state. + """ self._override = None @property @@ -67,8 +104,8 @@ def cast(self) -> T_co: """Returns self, but cast to the type of the provided value. This helps to pass providers as input to other providers while avoiding type checking errors: - :example: + Example: class A: ... def create_b(a: A) -> B: ... @@ -77,17 +114,28 @@ class Container(BaseContainer): a_factory = Factory(A) b_factory1 = Factory(create_b, a_factory) # works, but mypy (or pyright, etc.) will complain b_factory2 = Factory(create_b, a_factory.cast) # works and passes type checking + """ return typing.cast(T_co, self) class AbstractResource(AbstractProvider[T_co], abc.ABC): + """Base class for Resource providers.""" + def __init__( self, creator: ResourceCreatorType[P, T_co], *args: P.args, **kwargs: P.kwargs, ) -> None: + """Create a new resource. + + Args: + creator: sync or async iterator or context manager that yields resource. + *args: arguments to pass to the creator. + **kwargs: keyword arguments to pass to the creator. + + """ super().__init__() self._creator: typing.Any if inspect.isasyncgenfunction(creator): @@ -112,6 +160,7 @@ def __init__( @abc.abstractmethod def _fetch_context(self) -> ResourceContext[T_co]: ... + @override async def async_resolve(self) -> T_co: if self._override: return typing.cast(T_co, self._override) @@ -147,6 +196,7 @@ async def async_resolve(self) -> T_co: return context.instance + @override def sync_resolve(self) -> T_co: if self._override: return typing.cast(T_co, self._override) @@ -182,13 +232,23 @@ def _get_value_from_object_by_dotted_path(obj: typing.Any, path: str) -> typing. class AttrGetter( AbstractProvider[T_co], ): + """Provides an attribute after resolving the wrapped provider.""" + __slots__ = "_attrs", "_provider" def __init__(self, provider: AbstractProvider[T_co], attr_name: str) -> None: + """Create a new AttrGetter instance. + + Args: + provider: provider to wrap. + attr_name: attribute name to resolve when the provider is resolved. + + """ super().__init__() self._provider = provider self._attrs = [attr_name] + @override def __getattr__(self, attr: str) -> "AttrGetter[T_co]": if attr.startswith("_"): msg = f"'{type(self)}' object has no attribute '{attr}'" @@ -196,12 +256,14 @@ def __getattr__(self, attr: str) -> "AttrGetter[T_co]": self._attrs.append(attr) return self - async def async_resolve(self) -> typing.Any: # noqa: ANN401 + @override + async def async_resolve(self) -> typing.Any: resolved_provider_object = await self._provider.async_resolve() attribute_path = ".".join(self._attrs) return _get_value_from_object_by_dotted_path(resolved_provider_object, attribute_path) - def sync_resolve(self) -> typing.Any: # noqa: ANN401 + @override + def sync_resolve(self) -> typing.Any: resolved_provider_object = self._provider.sync_resolve() attribute_path = ".".join(self._attrs) return _get_value_from_object_by_dotted_path(resolved_provider_object, attribute_path) diff --git a/that_depends/providers/collection.py b/that_depends/providers/collection.py index c976a56..a6190cc 100644 --- a/that_depends/providers/collection.py +++ b/that_depends/providers/collection.py @@ -1,5 +1,7 @@ import typing +from typing_extensions import override + from that_depends.providers.base import AbstractProvider @@ -7,39 +9,108 @@ class List(AbstractProvider[list[T_co]]): + """Provides multiple resources as a list. + + The `List` provider resolves multiple dependencies into a list. + + Example: + ```python + from that_depends import providers + + provider1 = providers.Factory(lambda: 1) + provider2 = providers.Factory(lambda: 2) + + list_provider = List(provider1, provider2) + + # Synchronous resolution + resolved_list = list_provider.sync_resolve() + print(resolved_list) # Output: [1, 2] + + # Asynchronous resolution + import asyncio + resolved_list_async = asyncio.run(list_provider.async_resolve()) + print(resolved_list_async) # Output: [1, 2] + ``` + + """ + __slots__ = ("_providers",) def __init__(self, *providers: AbstractProvider[T_co]) -> None: + """Create a new List provider instance. + + Args: + *providers: List of providers to resolve. + + """ super().__init__() self._providers: typing.Final = providers - def __getattr__(self, attr_name: str) -> typing.Any: # noqa: ANN401 + @override + def __getattr__(self, attr_name: str) -> typing.Any: msg = f"'{type(self)}' object has no attribute '{attr_name}'" raise AttributeError(msg) + @override async def async_resolve(self) -> list[T_co]: return [await x.async_resolve() for x in self._providers] + @override def sync_resolve(self) -> list[T_co]: return [x.sync_resolve() for x in self._providers] + @override async def __call__(self) -> list[T_co]: return await self.async_resolve() class Dict(AbstractProvider[dict[str, T_co]]): + """Provides multiple resources as a dictionary. + + The `Dict` provider resolves multiple named dependencies into a dictionary. + + Example: + ```python + from that_depends import providers + + provider1 = providers.Factory(lambda: 1) + provider2 = providers.Factory(lambda: 2) + + dict_provider = Dict(key1=provider1, key2=provider2) + + # Synchronous resolution + resolved_dict = dict_provider.sync_resolve() + print(resolved_dict) # Output: {"key1": 1, "key2": 2} + + # Asynchronous resolution + import asyncio + resolved_dict_async = asyncio.run(dict_provider.async_resolve()) + print(resolved_dict_async) # Output: {"key1": 1, "key2": 2} + ``` + + """ + __slots__ = ("_providers",) def __init__(self, **providers: AbstractProvider[T_co]) -> None: + """Create a new Dict provider instance. + + Args: + **providers: Dictionary of providers to resolve. + + """ super().__init__() self._providers: typing.Final = providers - def __getattr__(self, attr_name: str) -> typing.Any: # noqa: ANN401 + @override + def __getattr__(self, attr_name: str) -> typing.Any: msg = f"'{type(self)}' object has no attribute '{attr_name}'" raise AttributeError(msg) + @override async def async_resolve(self) -> dict[str, T_co]: return {key: await provider.async_resolve() for key, provider in self._providers.items()} + @override def sync_resolve(self) -> dict[str, T_co]: return {key: provider.sync_resolve() for key, provider in self._providers.items()} diff --git a/that_depends/providers/context_resources.py b/that_depends/providers/context_resources.py index 7d47903..83e54f8 100644 --- a/that_depends/providers/context_resources.py +++ b/that_depends/providers/context_resources.py @@ -1,15 +1,18 @@ import abc +import asyncio import contextlib import inspect import logging +import threading import typing from abc import abstractmethod from contextlib import AbstractAsyncContextManager, AbstractContextManager from contextvars import ContextVar, Token from functools import wraps from types import TracebackType +from typing import Final -from typing_extensions import TypeIs +from typing_extensions import TypeIs, override from that_depends.entities.resource_context import ResourceContext from that_depends.meta import BaseContainerMeta @@ -44,6 +47,22 @@ def _get_container_context() -> dict[str, typing.Any]: def fetch_context_item(key: str, default: typing.Any = None) -> typing.Any: # noqa: ANN401 + """Retrieve a value from the global context. + + Args: + key (str): The key to retrieve from the global context. + default (Any): The default value to return if the key is not found. + + Returns: + Any: The value associated with the key in the global context or the default value. + + Example: + ```python + async with container_context(global_context={"username": "john_doe"}): + user = fetch_context_item("username") + ``` + + """ return _get_container_context().get(key, default) @@ -52,33 +71,84 @@ def fetch_context_item(key: str, default: typing.Any = None) -> typing.Any: # n class SupportsContext(typing.Generic[CT], abc.ABC): + """Interface for resources that support context initialization. + + This interface defines methods to create synchronous and asynchronous + context managers, as well as a function decorator for context initialization. + """ + @abstractmethod def context(self, func: typing.Callable[P, T]) -> typing.Callable[P, T]: - """Initialize context for the given function. + """Wrap a function with a new context. + + The returned function will automatically initialize and tear down + the context whenever it is called. + + Args: + func (Callable[P, T]): The function to wrap. + + Returns: + Callable[P, T]: The wrapped function. + + Example: + ```python + @my_resource.context + def my_function(): + return do_something() + ``` - :param func: function to wrap. - :return: wrapped function with context. """ @abstractmethod def async_context(self) -> typing.AsyncContextManager[CT]: - """Initialize async context.""" + """Create an async context manager for this resource. + + Returns: + AsyncContextManager[CT]: An async context manager. + + Example: + ```python + async with my_resource.async_context(): + result = await my_resource.async_resolve() + ``` + + """ @abstractmethod def sync_context(self) -> typing.ContextManager[CT]: - """Initialize sync context.""" + """Create a sync context manager for this resource. + + Returns: + ContextManager[CT]: A sync context manager. + + Example: + ```python + with my_resource.sync_context(): + result = my_resource.sync_resolve() + ``` + + """ @abstractmethod def supports_sync_context(self) -> bool: - """Check if the resource supports sync context.""" + """Check whether the resource supports sync context. + + Returns: + bool: True if sync context is supported, False otherwise. + + """ class ContextResource( AbstractResource[T_co], - AbstractAsyncContextManager[ResourceContext[T_co]], - AbstractContextManager[ResourceContext[T_co]], SupportsContext[ResourceContext[T_co]], ): + """A context-dependent provider that resolves resources only if their context is initialized. + + `ContextResource` handles both synchronous and asynchronous resource creators + and ensures they are properly torn down when the context exits. + """ + __slots__ = ( "_args", "_context", @@ -96,45 +166,52 @@ def __init__( *args: P.args, **kwargs: P.kwargs, ) -> None: + """Initialize a new context resource. + + Args: + creator (Callable[P, Iterator[T_co] | AsyncIterator[T_co]]): + A sync or async iterator that yields the resource to be provided. + *args: Positional arguments to pass to the creator. + **kwargs: Keyword arguments to pass to the creator. + + """ super().__init__(creator, *args, **kwargs) self._context: ContextVar[ResourceContext[T_co]] = ContextVar(f"{self._creator.__name__}-context") self._token: Token[ResourceContext[T_co]] | None = None + self._async_lock: Final = asyncio.Lock() + self._lock: Final = threading.Lock() + @override def supports_sync_context(self) -> bool: return not self.is_async - def __enter__(self) -> ResourceContext[T_co]: + def _enter_sync_context(self) -> ResourceContext[T_co]: if self.is_async: msg = "You must enter async context for async creators." raise RuntimeError(msg) return self._enter() - async def __aenter__(self) -> ResourceContext[T_co]: + async def _enter_async_context(self) -> ResourceContext[T_co]: return self._enter() def _enter(self) -> ResourceContext[T_co]: self._token = self._context.set(ResourceContext(is_async=self.is_async)) return self._context.get() - def __exit__( - self, exc_type: type[BaseException] | None, exc_value: BaseException | None, traceback: TracebackType | None - ) -> None: + def _exit_sync_context(self) -> None: if not self._token: - msg = "Context is not set, call ``__enter__`` first" + msg = "Context is not set, call ``_enter_sync_context`` first" raise RuntimeError(msg) try: context_item = self._context.get() context_item.sync_tear_down() - finally: self._context.reset(self._token) - async def __aexit__( - self, exc_type: type[BaseException] | None, exc_val: BaseException | None, traceback: TracebackType | None - ) -> None: + async def _exit_async_context(self) -> None: if self._token is None: - msg = "Context is not set, call ``__aenter__`` first" + msg = "Context is not set, call ``_enter_async_context`` first" raise RuntimeError(msg) try: @@ -147,27 +224,43 @@ async def __aexit__( self._context.reset(self._token) @contextlib.contextmanager + @override def sync_context(self) -> typing.Iterator[ResourceContext[T_co]]: if self.is_async: msg = "Please use async context instead." raise RuntimeError(msg) token = self._token - with self as val: - yield val + with self._lock: + val = self._enter_sync_context() + temp_token = self._token + yield val + with self._lock: + self._token = temp_token + self._exit_sync_context() self._token = token @contextlib.asynccontextmanager + @override async def async_context(self) -> typing.AsyncIterator[ResourceContext[T_co]]: token = self._token - async with self as val: - yield val + + async with self._async_lock: + val = await self._enter_async_context() + temp_token = self._token + yield val + async with self._async_lock: + self._token = temp_token + await self._exit_async_context() self._token = token + @override def context(self, func: typing.Callable[P, T]) -> typing.Callable[P, T]: """Create a new context manager for the resource, the context manager will be async if the resource is async. - :return: A context manager for the resource. - :rtype: typing.ContextManager[ResourceContext[T_co]] | typing.AsyncContextManager[ResourceContext[T_co]] + Returns: + typing.ContextManager[ResourceContext[T_co]] | typing.AsyncContextManager[ResourceContext[T_co]]: + A context manager for the resource. + """ if inspect.iscoroutinefunction(func): @@ -198,6 +291,12 @@ def _fetch_context(self) -> ResourceContext[T_co]: class container_context(AbstractContextManager[ContextType], AbstractAsyncContextManager[ContextType]): # noqa: N801 + """Initialize contexts for the provided containers or resources. + + Use this class to manage global and resource-specific contexts in both + synchronous and asynchronous scenarios. + """ + ___slots__ = ( "_providers", "_context_stack", @@ -216,11 +315,18 @@ def __init__( ) -> None: """Initialize a new container context. - :param context_items: context items to initialize new context for. - :param global_context: existing context to use - :param preserve_global_context: whether to preserve old global context. - Will merge old context with the new context if this option is set to True. - :param reset_all_containers: Create a new context for all containers. + Args: + *context_items (SupportsContext[Any]): Context items to initialize a new context for. + global_context (dict[str, Any] | None): A dictionary representing the global context. + preserve_global_context (bool): If True, merges the existing global context with the new one. + reset_all_containers (bool): If True, creates a new context for all containers in this scope. + + Example: + ```python + async with container_context(MyContainer, global_context={"key": "value"}): + data = fetch_context_item("key") + ``` + """ if preserve_global_context and global_context: self._initial_context = {**_get_container_context(), **global_context} @@ -244,6 +350,7 @@ def _add_providers_from_containers(self, containers: list[ContainerType]) -> Non if isinstance(container_provider, ContextResource): self._context_items.add(container_provider) + @override def __enter__(self) -> ContextType: self._context_stack = contextlib.ExitStack() for item in self._context_items: @@ -251,6 +358,7 @@ def __enter__(self) -> ContextType: self._context_stack.enter_context(item.sync_context()) return self._enter_globals() + @override async def __aenter__(self) -> ContextType: self._context_stack = contextlib.AsyncExitStack() for item in self._context_items: @@ -281,6 +389,7 @@ def _has_sync_exit_stack( ) -> typing.TypeGuard[contextlib.ExitStack]: return isinstance(_, contextlib.ExitStack) + @override def __exit__( self, exc_type: type[BaseException] | None, exc_value: BaseException | None, traceback: TracebackType | None ) -> None: @@ -293,6 +402,7 @@ def __exit__( finally: self._exit_globals() + @override async def __aexit__( self, exc_type: type[BaseException] | None, exc_val: BaseException | None, traceback: TracebackType | None ) -> None: @@ -306,6 +416,26 @@ async def __aexit__( self._exit_globals() def __call__(self, func: typing.Callable[P, T_co]) -> typing.Callable[P, T_co]: + """Decorate a function to run within this container context. + + The context is automatically initialized before the function is called and + torn down afterwards. + + Args: + func (Callable[P, T_co]): A sync or async callable. + + Returns: + Callable[P, T_co]: The wrapped function. + + Example: + ```python + @container_context(MyContainer) + async def my_async_function(): + result = await MyContainer.some_resource.async_resolve() + return result + ``` + + """ if inspect.iscoroutinefunction(func): @wraps(func) @@ -324,19 +454,55 @@ def _sync_inner(*args: P.args, **kwargs: P.kwargs) -> T_co: class DIContextMiddleware: + """ASGI middleware that manages context initialization for incoming requests. + + This middleware automatically creates and tears down context for each request, + ensuring that resources defined in containers or as context items are properly + initialized and cleaned up. + """ + def __init__( self, app: ASGIApp, *context_items: SupportsContext[typing.Any], global_context: dict[str, typing.Any] | None = None, - reset_all_containers: bool = True, + reset_all_containers: bool = False, ) -> None: + """Initialize the DIContextMiddleware. + + Args: + app (ASGIApp): The ASGI application to wrap. + *context_items (SupportsContext[Any]): A collection of containers and providers that + need context initialization prior to a request. + global_context (dict[str, Any] | None): A global context dictionary to set before requests. + reset_all_containers (bool): Whether to reset all containers in the current scope before the request. + + Example: + ```python + my_app.add_middleware(DIContextMiddleware, MyContainer, global_context={"api_key": "secret"}) + ``` + + """ self.app: typing.Final = app self._context_items: set[SupportsContext[typing.Any]] = set(context_items) self._global_context: dict[str, typing.Any] | None = global_context self._reset_all_containers: bool = reset_all_containers async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: + """Handle the incoming ASGI request by initializing and tearing down context. + + The context is initialized before the request is processed and + closed after the request is completed. + + Args: + scope (Scope): The ASGI scope. + receive (Receive): The receive call. + send (Send): The send call. + + Returns: + None + + """ if self._context_items: pass async with ( diff --git a/that_depends/providers/factories.py b/that_depends/providers/factories.py index 4acb9db..01d1e93 100644 --- a/that_depends/providers/factories.py +++ b/that_depends/providers/factories.py @@ -1,6 +1,8 @@ import abc import typing +from typing_extensions import override + from that_depends.providers.base import AbstractProvider @@ -9,24 +11,84 @@ class AbstractFactory(AbstractProvider[T_co], abc.ABC): + """Base class for all factories. + + This class defines the interface for factories that provide + resources both synchronously and asynchronously. + """ + @property def provider(self) -> typing.Callable[[], typing.Coroutine[typing.Any, typing.Any, T_co]]: + """Returns an async provider function. + + The async provider function can be awaited to resolve the resource. + + Returns: + Callable[[], Coroutine[Any, Any, T_co]]: The async provider function. + + Example: + ```python + async_provider = my_factory.provider + resource = await async_provider() + ``` + + """ return self.async_resolve @property def sync_provider(self) -> typing.Callable[[], T_co]: + """Return a sync provider function. + + The sync provider function can be called to resolve the resource synchronously. + + Returns: + Callable[[], T_co]: The sync provider function. + + Example: + ```python + sync_provider = my_factory.sync_provider + resource = sync_provider() + ``` + + """ return self.sync_resolve class Factory(AbstractFactory[T_co]): + """Provides an instance by calling a sync method. + + A typical usage scenario is to wrap a synchronous function + that returns a resource. Each call to the provider or sync_provider + produces a new instance of that resource. + + Example: + ```python + def build_resource(text: str, number: int): + return f"{text}-{number}" + + factory = Factory(build_resource, "example", 42) + resource = factory.sync_provider() # "example-42" + ``` + + """ + __slots__ = "_args", "_factory", "_kwargs", "_override" def __init__(self, factory: typing.Callable[P, T_co], *args: P.args, **kwargs: P.kwargs) -> None: + """Initialize a Factory instance. + + Args: + factory (Callable[P, T_co]): Function that returns the resource. + *args: Arguments to pass to the factory function. + **kwargs: Keyword arguments to pass to the factory function. + + """ super().__init__() self._factory: typing.Final = factory self._args: typing.Final = args self._kwargs: typing.Final = kwargs + @override async def async_resolve(self) -> T_co: if self._override: return typing.cast(T_co, self._override) @@ -40,6 +102,7 @@ async def async_resolve(self) -> T_co: }, ) + @override def sync_resolve(self) -> T_co: if self._override: return typing.cast(T_co, self._override) @@ -55,14 +118,40 @@ def sync_resolve(self) -> T_co: class AsyncFactory(AbstractFactory[T_co]): + """Provides an instance by calling an async method. + + Similar to `Factory`, but requires an async function. Each call + to the provider or `provider` property is awaited to produce a new instance. + + Example: + ```python + async def async_build_resource(text: str): + await some_async_operation() + return text.upper() + + async_factory = AsyncFactory(async_build_resource, "example") + resource = await async_factory.provider() # "EXAMPLE" + ``` + + """ + __slots__ = "_args", "_factory", "_kwargs", "_override" def __init__(self, factory: typing.Callable[P, typing.Awaitable[T_co]], *args: P.args, **kwargs: P.kwargs) -> None: + """Initialize an AsyncFactory instance. + + Args: + factory (Callable[P, Awaitable[T_co]]): Async function that returns the resource. + *args: Arguments to pass to the async factory function. + **kwargs: Keyword arguments to pass to the async factory function. + + """ super().__init__() self._factory: typing.Final = factory self._args: typing.Final = args self._kwargs: typing.Final = kwargs + @override async def async_resolve(self) -> T_co: if self._override: return typing.cast(T_co, self._override) @@ -76,6 +165,7 @@ async def async_resolve(self) -> T_co: }, ) + @override def sync_resolve(self) -> typing.NoReturn: msg = "AsyncFactory cannot be resolved synchronously" raise RuntimeError(msg) diff --git a/that_depends/providers/object.py b/that_depends/providers/object.py index 2b218a3..e49c497 100644 --- a/that_depends/providers/object.py +++ b/that_depends/providers/object.py @@ -1,5 +1,7 @@ import typing +from typing_extensions import override + from that_depends.providers.base import AbstractProvider @@ -8,15 +10,37 @@ class Object(AbstractProvider[T_co]): + """Provides an object "as is" without any modification. + + This provider always returns the same object that was given during + initialization. + + Example: + ```python + provider = Object(1) + result = provider.sync_resolve() + print(result) # 1 + ``` + + """ + __slots__ = ("_obj",) def __init__(self, obj: T_co) -> None: + """Initialize the provider with the given object. + + Args: + obj (T_co): The object to be provided. + + """ super().__init__() self._obj: typing.Final = obj + @override async def async_resolve(self) -> T_co: return self.sync_resolve() + @override def sync_resolve(self) -> T_co: if self._override is not None: return typing.cast(T_co, self._override) diff --git a/that_depends/providers/resources.py b/that_depends/providers/resources.py index 238ab5b..b75cc2e 100644 --- a/that_depends/providers/resources.py +++ b/that_depends/providers/resources.py @@ -9,6 +9,33 @@ class Resource(AbstractResource[T_co]): + """Provides a resource that is resolved once and cached for future usage. + + Unlike a singleton, this provider includes finalization logic and can be + used with a generator or async generator to manage resource lifecycle. + It also supports usage with `typing.ContextManager` or `typing.AsyncContextManager`. + Threading and asyncio concurrency are supported, ensuring only one instance + is created regardless of concurrent resolves. + + Example: + ```python + async def create_async_resource(): + try: + yield "async resource" + finally: + # Finalize resource + pass + + class MyContainer: + async_resource = Resource(create_async_resource) + + async def main(): + async_resource_instance = await MyContainer.async_resource.async_resolve() + await MyContainer.async_resource.tear_down() + ``` + + """ + __slots__ = ( "_args", "_context", @@ -26,6 +53,31 @@ def __init__( *args: P.args, **kwargs: P.kwargs, ) -> None: + """Initialize the Resource provider with a callable for resource creation. + + The callable can be a generator or async generator that yields the resource + (with optional teardown logic), or a context manager. Only one instance will be + created and cached until explicitly torn down. + + Args: + creator: The callable, generator, or context manager that creates the resource. + *args: Positional arguments passed to the creator. + **kwargs: Keyword arguments passed to the creator. + + Example: + ```python + def custom_creator(name: str): + try: + yield f"Resource created for {name}" + finally: + pass # Teardown + + resource_provider = Resource(custom_creator, "example") + instance = resource_provider.sync_resolve() + resource_provider.tear_down() + ``` + + """ super().__init__(creator, *args, **kwargs) self._context: typing.Final[ResourceContext[T_co]] = ResourceContext(is_async=self.is_async) @@ -33,4 +85,16 @@ def _fetch_context(self) -> ResourceContext[T_co]: return self._context async def tear_down(self) -> None: + """Tear down the resource if it has been created. + + If the resource was never resolved, or was already torn down, + calling this method has no effect. + + Example: + ```python + # Assuming my_provider was previously resolved + await my_provider.tear_down() + ``` + + """ await self._fetch_context().tear_down() diff --git a/that_depends/providers/selector.py b/that_depends/providers/selector.py index db63f1a..be7d2b3 100644 --- a/that_depends/providers/selector.py +++ b/that_depends/providers/selector.py @@ -1,5 +1,9 @@ +"""Selection based providers.""" + import typing +from typing_extensions import override + from that_depends.providers.base import AbstractProvider @@ -7,13 +11,61 @@ class Selector(AbstractProvider[T_co]): + """Chooses a provider based on a key returned by a selector function. + + This class allows you to dynamically select and resolve one of several + named providers at runtime. The provider key is determined by a + user-supplied selector function. + + Examples: + ```python + def environment_selector(): + return "local" + + selector_instance = Selector( + environment_selector, + local=LocalStorageProvider(), + remote=RemoteStorageProvider(), + ) + + # Synchronously resolve the selected provider + service = selector_instance.sync_resolve() + ``` + + """ + __slots__ = "_override", "_providers", "_selector" def __init__(self, selector: typing.Callable[[], str], **providers: AbstractProvider[T_co]) -> None: + """Initialize a new Selector instance. + + Args: + selector (Callable[[], str]): A function that returns the key + of the provider to use. + **providers (AbstractProvider[T_co]): The named providers from + which one will be selected based on the `selector`. + + Examples: + ```python + def my_selector(): + return "remote" + + my_selector_instance = Selector( + my_selector, + local=LocalStorageProvider(), + remote=RemoteStorageProvider(), + ) + + # The "remote" provider will be selected + selected_service = my_selector_instance.sync_resolve() + ``` + + """ super().__init__() self._selector: typing.Final = selector self._providers: typing.Final = providers + @override async def async_resolve(self) -> T_co: if self._override: return typing.cast(T_co, self._override) @@ -24,6 +76,7 @@ async def async_resolve(self) -> T_co: raise RuntimeError(msg) return await self._providers[selected_key].async_resolve() + @override def sync_resolve(self) -> T_co: if self._override: return typing.cast(T_co, self._override) diff --git a/that_depends/providers/singleton.py b/that_depends/providers/singleton.py index 78f4f15..5cd4332 100644 --- a/that_depends/providers/singleton.py +++ b/that_depends/providers/singleton.py @@ -1,7 +1,11 @@ +"""Resource providers.""" + import asyncio import threading import typing +from typing_extensions import override + from that_depends.providers.base import AbstractProvider @@ -10,9 +14,36 @@ class Singleton(AbstractProvider[T_co]): + """A provider that creates an instance once and caches it for subsequent injections. + + This provider is safe to use concurrently in both threading and asyncio contexts. + On the first call to either ``sync_resolve()`` or ``async_resolve()``, the instance + is created by calling the provided factory. All future calls return the cached instance. + + Example: + ```python + def my_factory() -> float: + return 0.5 + + singleton = Singleton(my_factory) + value1 = singleton.sync_resolve() + value2 = singleton.sync_resolve() + assert value1 == value2 + ``` + + """ + __slots__ = "_args", "_asyncio_lock", "_factory", "_instance", "_kwargs", "_override", "_threading_lock" def __init__(self, factory: typing.Callable[P, T_co], *args: P.args, **kwargs: P.kwargs) -> None: + """Initialize the Singleton provider. + + Args: + factory: A callable that produces the instance to be provided. + *args: Positional arguments to pass to the factory. + **kwargs: Keyword arguments to pass to the factory. + + """ super().__init__() self._factory: typing.Final = factory self._args: typing.Final = args @@ -21,6 +52,7 @@ def __init__(self, factory: typing.Callable[P, T_co], *args: P.args, **kwargs: P self._asyncio_lock: typing.Final = asyncio.Lock() self._threading_lock: typing.Final = threading.Lock() + @override async def async_resolve(self) -> T_co: if self._override is not None: return typing.cast(T_co, self._override) @@ -44,6 +76,7 @@ async def async_resolve(self) -> T_co: ) return self._instance + @override def sync_resolve(self) -> T_co: if self._override is not None: return typing.cast(T_co, self._override) @@ -67,14 +100,50 @@ def sync_resolve(self) -> T_co: return self._instance async def tear_down(self) -> None: + """Reset the cached instance. + + After calling this method, the next resolve call will recreate the instance. + """ if self._instance is not None: self._instance = None class AsyncSingleton(AbstractProvider[T_co]): + """A provider that creates an instance asynchronously and caches it for subsequent injections. + + This provider is safe to use concurrently in asyncio contexts. On the first call + to ``async_resolve()``, the instance is created by awaiting the provided factory. + All subsequent calls return the cached instance. + + Example: + ```python + async def my_async_factory() -> float: + return 0.5 + + async_singleton = AsyncSingleton(my_async_factory) + value1 = await async_singleton.async_resolve() + value2 = await async_singleton.async_resolve() + assert value1 == value2 + ``` + + """ + __slots__ = "_args", "_asyncio_lock", "_factory", "_instance", "_kwargs", "_override" - def __init__(self, factory: typing.Callable[P, typing.Awaitable[T_co]], *args: P.args, **kwargs: P.kwargs) -> None: + def __init__( + self, + factory: typing.Callable[P, typing.Awaitable[T_co]], + *args: P.args, + **kwargs: P.kwargs, + ) -> None: + """Initialize the AsyncSingleton provider. + + Args: + factory: The asynchronous callable used to create the instance. + *args: Positional arguments to pass to the factory. + **kwargs: Keyword arguments to pass to the factory. + + """ super().__init__() self._factory: typing.Final[typing.Callable[P, typing.Awaitable[T_co]]] = factory self._args: typing.Final[P.args] = args @@ -82,6 +151,7 @@ def __init__(self, factory: typing.Callable[P, typing.Awaitable[T_co]], *args: P self._instance: T_co | None = None self._asyncio_lock: typing.Final = asyncio.Lock() + @override async def async_resolve(self) -> T_co: if self._override is not None: return typing.cast(T_co, self._override) @@ -103,10 +173,15 @@ async def async_resolve(self) -> T_co: ) return self._instance + @override def sync_resolve(self) -> typing.NoReturn: msg = "AsyncSingleton cannot be resolved in an sync context." raise RuntimeError(msg) async def tear_down(self) -> None: + """Reset the cached instance. + + After calling this method, the next call to ``async_resolve()`` will recreate the instance. + """ if self._instance is not None: self._instance = None