From 403860aec1990c1adec35fbbcd20213ec1d14c48 Mon Sep 17 00:00:00 2001 From: elina-israyelyan Date: Wed, 22 Oct 2025 18:55:04 +0400 Subject: [PATCH 1/7] add context local resource with minimal tests --- src/dependency_injector/providers.pxd | 7 + src/dependency_injector/providers.pyi | 2 + src/dependency_injector/providers.pyx | 129 ++++- .../test_context_local_resource_py38.py | 478 ++++++++++++++++++ 4 files changed, 615 insertions(+), 1 deletion(-) create mode 100644 tests/unit/providers/resource/test_context_local_resource_py38.py diff --git a/src/dependency_injector/providers.pxd b/src/dependency_injector/providers.pxd index 21ed7f22..50c16a27 100644 --- a/src/dependency_injector/providers.pxd +++ b/src/dependency_injector/providers.pxd @@ -239,6 +239,13 @@ cdef class Resource(Provider): cpdef object _provide(self, tuple args, dict kwargs) +cdef class ContextLocalResource(Resource): + cdef object _resource_context_var + cdef object _shutdowner_context_var + + cpdef object _provide(self, tuple args, dict kwargs) + + cdef class Container(Provider): cdef object _container_cls cdef dict _overriding_providers diff --git a/src/dependency_injector/providers.pyi b/src/dependency_injector/providers.pyi index 8f9b525a..d6168d64 100644 --- a/src/dependency_injector/providers.pyi +++ b/src/dependency_injector/providers.pyi @@ -525,6 +525,8 @@ class Resource(Provider[T]): def init(self) -> Optional[Awaitable[T]]: ... def shutdown(self) -> Optional[Awaitable]: ... +class ContextLocalResource(Resource[T]):... + class Container(Provider[T]): def __init__( self, diff --git a/src/dependency_injector/providers.pyx b/src/dependency_injector/providers.pyx index d8a8ab35..045b8dc7 100644 --- a/src/dependency_injector/providers.pyx +++ b/src/dependency_injector/providers.pyx @@ -3186,7 +3186,7 @@ cdef class ThreadLocalSingleton(BaseSingleton): return future_result self._storage.instance = instance - + return instance def _async_init_instance(self, future_result, result): @@ -3867,6 +3867,133 @@ cdef class Resource(Provider): return self._resource +cdef class ContextLocalResource(Resource): + _none = object() + + def __init__(self, provides=None, *args, **kwargs): + self._resource_context_var = ContextVar("_resource_context_var", default=self._none) + self._shutdowner_context_var = ContextVar("_shutdowner_context_var", default=self._none) + super().__init__(provides, *args, **kwargs) + + def __deepcopy__(self, memo): + """Create and return full copy of provider.""" + copied = memo.get(id(self)) + if copied is not None: + return copied + + if self._resource_context_var.get() != self._none: + raise Error("Can not copy initialized resource") + copied = _memorized_duplicate(self, memo) + copied.set_provides(_copy_if_provider(self.provides, memo)) + copied.set_args(*deepcopy_args(self, self.args, memo)) + copied.set_kwargs(**deepcopy_kwargs(self, self.kwargs, memo)) + + self._copy_overridings(copied, memo) + + return copied + + @property + def initialized(self): + """Check if resource is initialized.""" + return self._resource_context_var.get() != self._none + + + def shutdown(self): + """Shutdown resource.""" + if self._resource_context_var.get() == self._none : + self._reset_all_contex_vars() + if self._async_mode == ASYNC_MODE_ENABLED: + return NULL_AWAITABLE + return + if self._shutdowner_context_var.get(): + future = self._shutdowner_context_var.get()(None, None, None) + if __is_future_or_coroutine(future): + self._reset_all_contex_vars() + return ensure_future(self._shutdown_async(future)) + + + self._reset_all_contex_vars() + if self._async_mode == ASYNC_MODE_ENABLED: + return NULL_AWAITABLE + + def _reset_all_contex_vars(self): + self._resource_context_var.set(self._none) + self._shutdowner_context_var.set(self._none) + + + async def _shutdown_async(self, future) -> None: + await future + + + async def _handle_async_cm(self, obj) -> None: + resource = await obj.__aenter__() + return resource + + async def _provide_async(self, future): + try: + obj = await future + + if hasattr(obj, '__aenter__') and hasattr(obj, '__aexit__'): + resource = await obj.__aenter__() + shutdowner = obj.__aexit__ + elif hasattr(obj, '__enter__') and hasattr(obj, '__exit__'): + resource = obj.__enter__() + shutdowner = obj.__exit__ + else: + resource = obj + shutdowner = None + + return resource, shutdowner + except: + raise + + cpdef object _provide(self, tuple args, dict kwargs): + if self._resource_context_var.get() != self._none: + return self._resource_context_var.get() + obj = __call( + self._provides, + args, + self._args, + self._args_len, + kwargs, + self._kwargs, + self._kwargs_len, + self._async_mode, + ) + + if __is_future_or_coroutine(obj): + future_result = asyncio.Future() + future = ensure_future(self._provide_async(obj)) + future.add_done_callback(functools.partial(self._async_init_instance, future_result)) + return future_result + elif hasattr(obj, '__enter__') and hasattr(obj, '__exit__'): + resource = obj.__enter__() + self._resource_context_var.set(resource) + self._shutdowner_context_var.set(obj.__exit__) + elif hasattr(obj, '__aenter__') and hasattr(obj, '__aexit__'): + resource = ensure_future(self._handle_async_cm(obj)) + self._resource_context_var.set(resource) + self._shutdowner_context_var.set(obj.__aexit__) + return resource + else: + self._resource_context_var.set(obj) + self._shutdowner_context_var.set(None) + + return self._resource_context_var.get() + + def _async_init_instance(self, future_result, result): + try: + resource, shutdowner = result.result() + except Exception as exception: + self._resource_context_var.set(self._none) + self._shutdowner_context_var.set(self._none) + future_result.set_exception(exception) + else: + self._resource_context_var.set(resource) + self._shutdowner_context_var.set(shutdowner) + future_result.set_result(resource) + + cdef class Container(Provider): """Container provider provides an instance of declarative container. diff --git a/tests/unit/providers/resource/test_context_local_resource_py38.py b/tests/unit/providers/resource/test_context_local_resource_py38.py new file mode 100644 index 00000000..63f3c9b6 --- /dev/null +++ b/tests/unit/providers/resource/test_context_local_resource_py38.py @@ -0,0 +1,478 @@ +"""Resource provider tests.""" + +import asyncio +import decimal +import sys +from contextlib import contextmanager +from typing import Any + +from pytest import mark, raises + +from dependency_injector import containers, errors, providers, resources + +def init_fn(*args, **kwargs): + return args, kwargs + + +def test_is_provider(): + assert providers.is_provider(providers.ContextLocalResource(init_fn)) is True + + +def test_init_optional_provides(): + provider = providers.ContextLocalResource() + provider.set_provides(init_fn) + assert provider.provides is init_fn + assert provider() == (tuple(), dict()) + + +def test_set_provides_returns_(): + provider = providers.ContextLocalResource() + assert provider.set_provides(init_fn) is provider + + +@mark.parametrize( + "str_name,cls", + [ + ("dependency_injector.providers.Factory", providers.Factory), + ("decimal.Decimal", decimal.Decimal), + ("list", list), + (".test_context_local_resource_py38.test_is_provider", test_is_provider), + ("test_is_provider", test_is_provider), + ], +) +def test_set_provides_string_imports(str_name, cls): + print( providers.ContextLocalResource(str_name).provides) + print(cls) + assert providers.ContextLocalResource(str_name).provides is cls + + +def test_provided_instance_provider(): + provider = providers.ContextLocalResource(init_fn) + assert isinstance(provider.provided, providers.ProvidedInstance) + + +def test_injection(): + resource = object() + + def _init(): + _init.counter += 1 + return resource + + _init.counter = 0 + + class Container(containers.DeclarativeContainer): + context_local_resource = providers.ContextLocalResource(_init) + dependency1 = providers.List(context_local_resource) + dependency2 = providers.List(context_local_resource) + + container = Container() + list1 = container.dependency1() + list2 = container.dependency2() + + assert list1 == [resource] + assert list1[0] is resource + + assert list2 == [resource] + assert list2[0] is resource + + assert _init.counter == 1 + + +def test_injection_in_different_context(): + def _init(): + return object() + + async def _async_init(): + return object() + + + class Container(containers.DeclarativeContainer): + context_local_resource = providers.ContextLocalResource(_init) + async_context_local_resource = providers.ContextLocalResource(_async_init) + + loop = asyncio.get_event_loop() + container = Container() + obj1 = loop.run_until_complete(container.async_context_local_resource()) + obj2 = loop.run_until_complete(container.async_context_local_resource()) + assert obj1!=obj2 + + obj3 = container.context_local_resource() + obj4 = container.context_local_resource() + + assert obj3==obj4 + + + + +def test_init_function(): + def _init(): + _init.counter += 1 + + _init.counter = 0 + + provider = providers.ContextLocalResource(_init) + + result1 = provider() + assert result1 is None + assert _init.counter == 1 + + result2 = provider() + assert result2 is None + assert _init.counter == 1 + + provider.shutdown() + + +def test_init_generator(): + def _init(): + _init.init_counter += 1 + yield + _init.shutdown_counter += 1 + + _init.init_counter = 0 + _init.shutdown_counter = 0 + + provider = providers.ContextLocalResource(_init) + + result1 = provider() + assert result1 is None + assert _init.init_counter == 1 + assert _init.shutdown_counter == 0 + + provider.shutdown() + assert _init.init_counter == 1 + assert _init.shutdown_counter == 1 + + result2 = provider() + assert result2 is None + assert _init.init_counter == 2 + assert _init.shutdown_counter == 1 + + provider.shutdown() + assert _init.init_counter == 2 + assert _init.shutdown_counter == 2 + + +def test_init_context_manager() -> None: + init_counter, shutdown_counter = 0, 0 + + @contextmanager + def _init(): + nonlocal init_counter, shutdown_counter + + init_counter += 1 + yield + shutdown_counter += 1 + + init_counter = 0 + shutdown_counter = 0 + + provider = providers.ContextLocalResource(_init) + + result1 = provider() + assert result1 is None + assert init_counter == 1 + assert shutdown_counter == 0 + + provider.shutdown() + assert init_counter == 1 + assert shutdown_counter == 1 + + result2 = provider() + assert result2 is None + assert init_counter == 2 + assert shutdown_counter == 1 + + provider.shutdown() + assert init_counter == 2 + assert shutdown_counter == 2 + + +def test_init_class(): + class TestResource(resources.Resource): + init_counter = 0 + shutdown_counter = 0 + + def init(self): + self.__class__.init_counter += 1 + + def shutdown(self, _): + self.__class__.shutdown_counter += 1 + + provider = providers.ContextLocalResource(TestResource) + + result1 = provider() + assert result1 is None + assert TestResource.init_counter == 1 + assert TestResource.shutdown_counter == 0 + + provider.shutdown() + assert TestResource.init_counter == 1 + assert TestResource.shutdown_counter == 1 + + result2 = provider() + assert result2 is None + assert TestResource.init_counter == 2 + assert TestResource.shutdown_counter == 1 + + provider.shutdown() + assert TestResource.init_counter == 2 + assert TestResource.shutdown_counter == 2 + + +def test_init_class_generic_typing(): + # See issue: https://github.com/ets-labs/python-dependency-injector/issues/488 + class TestDependency: + ... + + class TestResource(resources.Resource[TestDependency]): + def init(self, *args: Any, **kwargs: Any) -> TestDependency: + return TestDependency() + + def shutdown(self, resource: TestDependency) -> None: ... + + assert issubclass(TestResource, resources.Resource) is True + + +def test_init_class_abc_init_definition_is_required(): + class TestResource(resources.Resource): + ... + + with raises(TypeError) as context: + TestResource() + + assert "Can't instantiate abstract class TestResource" in str(context.value) + assert "init" in str(context.value) + + +def test_init_class_abc_shutdown_definition_is_not_required(): + class TestResource(resources.Resource): + def init(self): + ... + + assert hasattr(TestResource(), "shutdown") is True + + +def test_init_not_callable(): + provider = providers.ContextLocalResource(1) + with raises(TypeError, match=r"object is not callable"): + provider.init() + + +def test_init_and_shutdown(): + def _init(): + _init.init_counter += 1 + yield + _init.shutdown_counter += 1 + + _init.init_counter = 0 + _init.shutdown_counter = 0 + + provider = providers.ContextLocalResource(_init) + + result1 = provider.init() + assert result1 is None + assert _init.init_counter == 1 + assert _init.shutdown_counter == 0 + + provider.shutdown() + assert _init.init_counter == 1 + assert _init.shutdown_counter == 1 + + result2 = provider.init() + assert result2 is None + assert _init.init_counter == 2 + assert _init.shutdown_counter == 1 + + provider.shutdown() + assert _init.init_counter == 2 + assert _init.shutdown_counter == 2 + + +def test_shutdown_of_not_initialized(): + def _init(): + yield + + provider = providers.ContextLocalResource(_init) + + result = provider.shutdown() + assert result is None + + +def test_initialized(): + provider = providers.ContextLocalResource(init_fn) + assert provider.initialized is False + + provider.init() + assert provider.initialized is True + + provider.shutdown() + assert provider.initialized is False + + +def test_call_with_context_args(): + provider = providers.ContextLocalResource(init_fn, "i1", "i2") + assert provider("i3", i4=4) == (("i1", "i2", "i3"), {"i4": 4}) + + +def test_fluent_interface(): + provider = providers.ContextLocalResource(init_fn) \ + .add_args(1, 2) \ + .add_kwargs(a3=3, a4=4) + assert provider() == ((1, 2), {"a3": 3, "a4": 4}) + + +def test_set_args(): + provider = providers.ContextLocalResource(init_fn) \ + .add_args(1, 2) \ + .set_args(3, 4) + assert provider.args == (3, 4) + + +def test_clear_args(): + provider = providers.ContextLocalResource(init_fn) \ + .add_args(1, 2) \ + .clear_args() + assert provider.args == tuple() + + +def test_set_kwargs(): + provider = providers.ContextLocalResource(init_fn) \ + .add_kwargs(a1="i1", a2="i2") \ + .set_kwargs(a3="i3", a4="i4") + assert provider.kwargs == {"a3": "i3", "a4": "i4"} + + +def test_clear_kwargs(): + provider = providers.ContextLocalResource(init_fn) \ + .add_kwargs(a1="i1", a2="i2") \ + .clear_kwargs() + assert provider.kwargs == {} + + +def test_call_overridden(): + provider = providers.ContextLocalResource(init_fn, 1) + overriding_provider1 = providers.ContextLocalResource(init_fn, 2) + overriding_provider2 = providers.ContextLocalResource(init_fn, 3) + + provider.override(overriding_provider1) + provider.override(overriding_provider2) + + instance1 = provider() + instance2 = provider() + + assert instance1 is instance2 + assert instance1 == ((3,), {}) + assert instance2 == ((3,), {}) + + +def test_deepcopy(): + provider = providers.ContextLocalResource(init_fn, 1, 2, a3=3, a4=4) + + provider_copy = providers.deepcopy(provider) + + assert provider is not provider_copy + assert provider.args == provider_copy.args + assert provider.kwargs == provider_copy.kwargs + assert isinstance(provider, providers.ContextLocalResource) + + +def test_deepcopy_initialized(): + provider = providers.ContextLocalResource(init_fn) + provider.init() + + with raises(errors.Error): + providers.deepcopy(provider) + + +def test_deepcopy_from_memo(): + provider = providers.ContextLocalResource(init_fn) + provider_copy_memo = providers.ContextLocalResource(init_fn) + + provider_copy = providers.deepcopy( + provider, + memo={id(provider): provider_copy_memo}, + ) + + assert provider_copy is provider_copy_memo + + +def test_deepcopy_args(): + provider = providers.ContextLocalResource(init_fn) + dependent_provider1 = providers.Factory(list) + dependent_provider2 = providers.Factory(dict) + + provider.add_args(dependent_provider1, dependent_provider2) + + provider_copy = providers.deepcopy(provider) + dependent_provider_copy1 = provider_copy.args[0] + dependent_provider_copy2 = provider_copy.args[1] + + assert provider.args != provider_copy.args + + assert dependent_provider1.cls is dependent_provider_copy1.cls + assert dependent_provider1 is not dependent_provider_copy1 + + assert dependent_provider2.cls is dependent_provider_copy2.cls + assert dependent_provider2 is not dependent_provider_copy2 + + +def test_deepcopy_kwargs(): + provider = providers.ContextLocalResource(init_fn) + dependent_provider1 = providers.Factory(list) + dependent_provider2 = providers.Factory(dict) + + provider.add_kwargs(d1=dependent_provider1, d2=dependent_provider2) + + provider_copy = providers.deepcopy(provider) + dependent_provider_copy1 = provider_copy.kwargs["d1"] + dependent_provider_copy2 = provider_copy.kwargs["d2"] + + assert provider.kwargs != provider_copy.kwargs + + assert dependent_provider1.cls is dependent_provider_copy1.cls + assert dependent_provider1 is not dependent_provider_copy1 + + assert dependent_provider2.cls is dependent_provider_copy2.cls + assert dependent_provider2 is not dependent_provider_copy2 + + +def test_deepcopy_overridden(): + provider = providers.ContextLocalResource(init_fn) + object_provider = providers.Object(object()) + + provider.override(object_provider) + + provider_copy = providers.deepcopy(provider) + object_provider_copy = provider_copy.overridden[0] + + assert provider is not provider_copy + assert provider.args == provider_copy.args + assert isinstance(provider, providers.ContextLocalResource) + + assert object_provider is not object_provider_copy + assert isinstance(object_provider_copy, providers.Object) + + +def test_deepcopy_with_sys_streams(): + provider = providers.ContextLocalResource(init_fn) + provider.add_args(sys.stdin, sys.stdout, sys.stderr) + + provider_copy = providers.deepcopy(provider) + + assert provider is not provider_copy + assert isinstance(provider_copy, providers.ContextLocalResource) + assert provider.args[0] is sys.stdin + assert provider.args[1] is sys.stdout + assert provider.args[2] is sys.stderr + + +def test_repr(): + provider = providers.ContextLocalResource(init_fn) + + assert repr(provider) == ( + "".format( + repr(init_fn), + hex(id(provider)), + ) + ) From 4838ea6aa89f95b41eae15a7b2a9e4e5329c51ea Mon Sep 17 00:00:00 2001 From: elina-israyelyan Date: Wed, 22 Oct 2025 23:26:10 +0400 Subject: [PATCH 2/7] remove redundant tests --- .../test_context_local_resource_py38.py | 34 ------------------- 1 file changed, 34 deletions(-) diff --git a/tests/unit/providers/resource/test_context_local_resource_py38.py b/tests/unit/providers/resource/test_context_local_resource_py38.py index 63f3c9b6..6fb85aed 100644 --- a/tests/unit/providers/resource/test_context_local_resource_py38.py +++ b/tests/unit/providers/resource/test_context_local_resource_py38.py @@ -41,8 +41,6 @@ def test_set_provides_returns_(): ], ) def test_set_provides_string_imports(str_name, cls): - print( providers.ContextLocalResource(str_name).provides) - print(cls) assert providers.ContextLocalResource(str_name).provides is cls @@ -220,38 +218,6 @@ def shutdown(self, _): assert TestResource.shutdown_counter == 2 -def test_init_class_generic_typing(): - # See issue: https://github.com/ets-labs/python-dependency-injector/issues/488 - class TestDependency: - ... - - class TestResource(resources.Resource[TestDependency]): - def init(self, *args: Any, **kwargs: Any) -> TestDependency: - return TestDependency() - - def shutdown(self, resource: TestDependency) -> None: ... - - assert issubclass(TestResource, resources.Resource) is True - - -def test_init_class_abc_init_definition_is_required(): - class TestResource(resources.Resource): - ... - - with raises(TypeError) as context: - TestResource() - - assert "Can't instantiate abstract class TestResource" in str(context.value) - assert "init" in str(context.value) - - -def test_init_class_abc_shutdown_definition_is_not_required(): - class TestResource(resources.Resource): - def init(self): - ... - - assert hasattr(TestResource(), "shutdown") is True - def test_init_not_callable(): provider = providers.ContextLocalResource(1) From 78cea35db99c35bd94da71d56fdde3deeb45f5b9 Mon Sep 17 00:00:00 2001 From: elina-israyelyan Date: Thu, 23 Oct 2025 00:32:52 +0400 Subject: [PATCH 3/7] fix shutdowner default none value, add more tests --- src/dependency_injector/providers.pyx | 4 +- .../test_context_local_resource_py38.py | 103 +++++++++++++----- 2 files changed, 77 insertions(+), 30 deletions(-) diff --git a/src/dependency_injector/providers.pyx b/src/dependency_injector/providers.pyx index 045b8dc7..f829bfba 100644 --- a/src/dependency_injector/providers.pyx +++ b/src/dependency_injector/providers.pyx @@ -3905,7 +3905,7 @@ cdef class ContextLocalResource(Resource): if self._async_mode == ASYNC_MODE_ENABLED: return NULL_AWAITABLE return - if self._shutdowner_context_var.get(): + if self._shutdowner_context_var.get() != self._none: future = self._shutdowner_context_var.get()(None, None, None) if __is_future_or_coroutine(future): self._reset_all_contex_vars() @@ -3977,7 +3977,7 @@ cdef class ContextLocalResource(Resource): return resource else: self._resource_context_var.set(obj) - self._shutdowner_context_var.set(None) + self._shutdowner_context_var.set(self._none) return self._resource_context_var.get() diff --git a/tests/unit/providers/resource/test_context_local_resource_py38.py b/tests/unit/providers/resource/test_context_local_resource_py38.py index 6fb85aed..2bcc0b9e 100644 --- a/tests/unit/providers/resource/test_context_local_resource_py38.py +++ b/tests/unit/providers/resource/test_context_local_resource_py38.py @@ -4,12 +4,12 @@ import decimal import sys from contextlib import contextmanager -from typing import Any from pytest import mark, raises from dependency_injector import containers, errors, providers, resources + def init_fn(*args, **kwargs): return args, kwargs @@ -76,30 +76,27 @@ class Container(containers.DeclarativeContainer): assert _init.counter == 1 -def test_injection_in_different_context(): +@mark.asyncio +async def test_injection_in_different_context(): def _init(): return object() async def _async_init(): return object() - class Container(containers.DeclarativeContainer): context_local_resource = providers.ContextLocalResource(_init) async_context_local_resource = providers.ContextLocalResource(_async_init) - loop = asyncio.get_event_loop() container = Container() - obj1 = loop.run_until_complete(container.async_context_local_resource()) - obj2 = loop.run_until_complete(container.async_context_local_resource()) - assert obj1!=obj2 + obj1 = await container.async_context_local_resource() + obj2 = await container.async_context_local_resource() + assert obj1 != obj2 obj3 = container.context_local_resource() obj4 = container.context_local_resource() - assert obj3==obj4 - - + assert obj3 == obj4 def test_init_function(): @@ -121,10 +118,10 @@ def _init(): provider.shutdown() -def test_init_generator(): +def test_init_generator_in_one_context(): def _init(): _init.init_counter += 1 - yield + yield object() _init.shutdown_counter += 1 _init.init_counter = 0 @@ -133,7 +130,10 @@ def _init(): provider = providers.ContextLocalResource(_init) result1 = provider() - assert result1 is None + result2 = provider() + + assert result1 == result2 + assert _init.init_counter == 1 assert _init.shutdown_counter == 0 @@ -141,17 +141,12 @@ def _init(): assert _init.init_counter == 1 assert _init.shutdown_counter == 1 - result2 = provider() - assert result2 is None - assert _init.init_counter == 2 - assert _init.shutdown_counter == 1 - provider.shutdown() - assert _init.init_counter == 2 - assert _init.shutdown_counter == 2 + assert _init.init_counter == 1 + assert _init.shutdown_counter == 1 -def test_init_context_manager() -> None: +def test_init_context_manager_in_one_context() -> None: init_counter, shutdown_counter = 0, 0 @contextmanager @@ -159,7 +154,7 @@ def _init(): nonlocal init_counter, shutdown_counter init_counter += 1 - yield + yield object() shutdown_counter += 1 init_counter = 0 @@ -168,24 +163,77 @@ def _init(): provider = providers.ContextLocalResource(_init) result1 = provider() - assert result1 is None + result2 = provider() + assert result1 == result2 + assert init_counter == 1 assert shutdown_counter == 0 provider.shutdown() + assert init_counter == 1 assert shutdown_counter == 1 - result2 = provider() - assert result2 is None - assert init_counter == 2 + provider.shutdown() + assert init_counter == 1 assert shutdown_counter == 1 - provider.shutdown() + +@mark.asyncio +async def test_async_init_context_manager_in_different_contexts() -> None: + init_counter, shutdown_counter = 0, 0 + + async def _init(): + nonlocal init_counter, shutdown_counter + init_counter += 1 + yield object() + shutdown_counter += 1 + + init_counter = 0 + shutdown_counter = 0 + + provider = providers.ContextLocalResource(_init) + + async def run_in_context(): + resource = await provider() + await provider.shutdown() + return resource + + result1, result2 = await asyncio.gather(run_in_context(), run_in_context()) + + assert result1 != result2 assert init_counter == 2 assert shutdown_counter == 2 +@mark.asyncio +async def test_async_init_context_manager_in_one_context() -> None: + init_counter, shutdown_counter = 0, 0 + + async def _init(): + nonlocal init_counter, shutdown_counter + init_counter += 1 + yield object() + shutdown_counter += 1 + + init_counter = 0 + shutdown_counter = 0 + + provider = providers.ContextLocalResource(_init) + + async def run_in_context(): + resource_1 = await provider() + resource_2 = await provider() + await provider.shutdown() + return resource_1, resource_2 + + result1, result2 = await run_in_context() + + assert result1 == result2 + assert init_counter == 1 + assert shutdown_counter == 1 + + def test_init_class(): class TestResource(resources.Resource): init_counter = 0 @@ -218,7 +266,6 @@ def shutdown(self, _): assert TestResource.shutdown_counter == 2 - def test_init_not_callable(): provider = providers.ContextLocalResource(1) with raises(TypeError, match=r"object is not callable"): From 9517467a98a1b1dccac641b62ab3dcd3fe502b96 Mon Sep 17 00:00:00 2001 From: elina-israyelyan Date: Thu, 23 Oct 2025 01:03:18 +0400 Subject: [PATCH 4/7] fix fast depends version to v2 --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 9c33d385..502ee3de 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -20,6 +20,6 @@ scipy boto3 mypy_boto3_s3 typing_extensions -fast-depends +fast-depends~=2.4.0 -r requirements-ext.txt From ead6ff22b81004b5978aebd79de355c145238704 Mon Sep 17 00:00:00 2001 From: elina-israyelyan Date: Thu, 23 Oct 2025 01:10:18 +0400 Subject: [PATCH 5/7] fix tox.ini fast depends version --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index cadccd84..7c7f15ab 100644 --- a/tox.ini +++ b/tox.ini @@ -17,7 +17,7 @@ deps= mypy_boto3_s3 pydantic-settings werkzeug - fast-depends + fast-depends~=2.4.0 extras= yaml commands = pytest From 1fea8e9d9845ca368115d184c0bd88b6d56c8e1e Mon Sep 17 00:00:00 2001 From: elina-israyelyan Date: Thu, 23 Oct 2025 01:51:03 +0400 Subject: [PATCH 6/7] add tests for closing --- tests/unit/samples/wiring/asyncinjections.py | 13 ++++++++- .../wiringstringids/asyncinjections.py | 13 ++++++++- .../test_async_injections_py36.py | 27 ++++++++++++++++--- .../string_ids/test_async_injections_py36.py | 27 ++++++++++++++++--- 4 files changed, 72 insertions(+), 8 deletions(-) diff --git a/tests/unit/samples/wiring/asyncinjections.py b/tests/unit/samples/wiring/asyncinjections.py index e0861017..befd59b0 100644 --- a/tests/unit/samples/wiring/asyncinjections.py +++ b/tests/unit/samples/wiring/asyncinjections.py @@ -18,6 +18,7 @@ def reset_counters(self): resource1 = TestResource() resource2 = TestResource() +resource3 = TestResource() async def async_resource(resource): @@ -34,6 +35,8 @@ class Container(containers.DeclarativeContainer): resource1 = providers.Resource(async_resource, providers.Object(resource1)) resource2 = providers.Resource(async_resource, providers.Object(resource2)) + context_local_resource = providers.ContextLocalResource(async_resource, providers.Object(resource3)) + context_local_resource_with_factory_object = providers.ContextLocalResource(async_resource, providers.Factory(TestResource)) @inject @@ -57,5 +60,13 @@ async def async_generator_injection( async def async_injection_with_closing( resource1: object = Closing[Provide[Container.resource1]], resource2: object = Closing[Provide[Container.resource2]], + context_local_resource: object = Closing[Provide[Container.context_local_resource]], ): - return resource1, resource2 + return resource1, resource2, context_local_resource + + +@inject +async def async_injection_with_closing_context_local_resources( + context_local_resource1: object = Closing[Provide[Container.context_local_resource_with_factory_object]], +): + return context_local_resource1 diff --git a/tests/unit/samples/wiringstringids/asyncinjections.py b/tests/unit/samples/wiringstringids/asyncinjections.py index 41529379..514b455a 100644 --- a/tests/unit/samples/wiringstringids/asyncinjections.py +++ b/tests/unit/samples/wiringstringids/asyncinjections.py @@ -16,6 +16,7 @@ def reset_counters(self): resource1 = TestResource() resource2 = TestResource() +resource3 = TestResource() async def async_resource(resource): @@ -32,6 +33,8 @@ class Container(containers.DeclarativeContainer): resource1 = providers.Resource(async_resource, providers.Object(resource1)) resource2 = providers.Resource(async_resource, providers.Object(resource2)) + context_local_resource = providers.ContextLocalResource(async_resource, providers.Object(resource3)) + context_local_resource_with_factory_object = providers.ContextLocalResource(async_resource, providers.Factory(TestResource)) @inject @@ -46,5 +49,13 @@ async def async_injection( async def async_injection_with_closing( resource1: object = Closing[Provide["resource1"]], resource2: object = Closing[Provide["resource2"]], + context_local_resource: object = Closing[Provide["context_local_resource"]], ): - return resource1, resource2 + return resource1, resource2, context_local_resource + + +@inject +async def async_injection_with_closing_context_local_resources( + context_local_resource1: object = Closing[Provide["context_local_resource_with_factory_object"]] +): + return context_local_resource1 diff --git a/tests/unit/wiring/provider_ids/test_async_injections_py36.py b/tests/unit/wiring/provider_ids/test_async_injections_py36.py index 70f9eb17..4c5ec12f 100644 --- a/tests/unit/wiring/provider_ids/test_async_injections_py36.py +++ b/tests/unit/wiring/provider_ids/test_async_injections_py36.py @@ -1,7 +1,8 @@ """Async injection tests.""" -from pytest import fixture, mark +import asyncio +from pytest import fixture, mark from samples.wiring import asyncinjections @@ -51,7 +52,7 @@ async def test_async_generator_injections() -> None: @mark.asyncio async def test_async_injections_with_closing(): - resource1, resource2 = await asyncinjections.async_injection_with_closing() + resource1, resource2, context_local_resource = await asyncinjections.async_injection_with_closing() assert resource1 is asyncinjections.resource1 assert asyncinjections.resource1.init_counter == 1 @@ -61,7 +62,11 @@ async def test_async_injections_with_closing(): assert asyncinjections.resource2.init_counter == 1 assert asyncinjections.resource2.shutdown_counter == 1 - resource1, resource2 = await asyncinjections.async_injection_with_closing() + assert context_local_resource is asyncinjections.resource3 + assert asyncinjections.resource3.init_counter == 1 + assert asyncinjections.resource3.shutdown_counter == 1 + + resource1, resource2, context_local_resource = await asyncinjections.async_injection_with_closing() assert resource1 is asyncinjections.resource1 assert asyncinjections.resource1.init_counter == 2 @@ -70,3 +75,19 @@ async def test_async_injections_with_closing(): assert resource2 is asyncinjections.resource2 assert asyncinjections.resource2.init_counter == 2 assert asyncinjections.resource2.shutdown_counter == 2 + + assert context_local_resource is asyncinjections.resource3 + assert asyncinjections.resource3.init_counter == 2 + assert asyncinjections.resource3.shutdown_counter == 2 + + +@mark.asyncio +async def test_async_injections_with_closing_concurrently(): + resource1, resource2 = await asyncio.gather(asyncinjections.async_injection_with_closing_context_local_resources(), + asyncinjections.async_injection_with_closing_context_local_resources()) + assert resource1 != resource2 + + resource1 = await asyncinjections.Container.context_local_resource_with_factory_object() + resource2 = await asyncinjections.Container.context_local_resource_with_factory_object() + + assert resource1 == resource2 diff --git a/tests/unit/wiring/string_ids/test_async_injections_py36.py b/tests/unit/wiring/string_ids/test_async_injections_py36.py index cff13ce5..bdf6a2ab 100644 --- a/tests/unit/wiring/string_ids/test_async_injections_py36.py +++ b/tests/unit/wiring/string_ids/test_async_injections_py36.py @@ -1,7 +1,8 @@ """Async injection tests.""" -from pytest import fixture, mark +import asyncio +from pytest import fixture, mark from samples.wiringstringids import asyncinjections @@ -34,7 +35,7 @@ async def test_async_injections(): @mark.asyncio async def test_async_injections_with_closing(): - resource1, resource2 = await asyncinjections.async_injection_with_closing() + resource1, resource2, context_local_resource = await asyncinjections.async_injection_with_closing() assert resource1 is asyncinjections.resource1 assert asyncinjections.resource1.init_counter == 1 @@ -44,7 +45,11 @@ async def test_async_injections_with_closing(): assert asyncinjections.resource2.init_counter == 1 assert asyncinjections.resource2.shutdown_counter == 1 - resource1, resource2 = await asyncinjections.async_injection_with_closing() + assert context_local_resource is asyncinjections.resource3 + assert asyncinjections.resource3.init_counter == 1 + assert asyncinjections.resource3.shutdown_counter == 1 + + resource1, resource2, context_local_resource = await asyncinjections.async_injection_with_closing() assert resource1 is asyncinjections.resource1 assert asyncinjections.resource1.init_counter == 2 @@ -53,3 +58,19 @@ async def test_async_injections_with_closing(): assert resource2 is asyncinjections.resource2 assert asyncinjections.resource2.init_counter == 2 assert asyncinjections.resource2.shutdown_counter == 2 + + assert context_local_resource is asyncinjections.resource3 + assert asyncinjections.resource3.init_counter == 2 + assert asyncinjections.resource3.shutdown_counter == 2 + + +@mark.asyncio +async def test_async_injections_with_closing_concurrently(): + resource1, resource2 = await asyncio.gather(asyncinjections.async_injection_with_closing_context_local_resources(), + asyncinjections.async_injection_with_closing_context_local_resources()) + assert resource1 != resource2 + + resource1 = await asyncinjections.Container.context_local_resource_with_factory_object() + resource2 = await asyncinjections.Container.context_local_resource_with_factory_object() + + assert resource1 == resource2 From bc7b4ebc376fccfa708741d54eeb049ef43ddb37 Mon Sep 17 00:00:00 2001 From: elina-israyelyan Date: Fri, 24 Oct 2025 13:36:01 +0400 Subject: [PATCH 7/7] revert fast depends version change --- requirements-dev.txt | 2 +- tox.ini | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 502ee3de..9c33d385 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -20,6 +20,6 @@ scipy boto3 mypy_boto3_s3 typing_extensions -fast-depends~=2.4.0 +fast-depends -r requirements-ext.txt diff --git a/tox.ini b/tox.ini index 7c7f15ab..cadccd84 100644 --- a/tox.ini +++ b/tox.ini @@ -17,7 +17,7 @@ deps= mypy_boto3_s3 pydantic-settings werkzeug - fast-depends~=2.4.0 + fast-depends extras= yaml commands = pytest