Repository: hynek/svcs (language: python)

Issue:
Async factories don't trigger context manager
```py
async def factory(c: Container):
    some_stuff = await c.aget(SomeOtherType)  # async factory because we want to do this
    return SomeType(some_stuff)

registry.register_factory(SomeType, factory)
```

Let's say `SomeType` is also context manager. The code won't enter/exit the context manager because [that only happens if the factory is not awaitable](https://github.com/hynek/svcs/blob/8ed3db3c8f742730a4d9e51a0d676b3c31423681/src/svcs/_core.py#L1008-L1015).
Async factories don't trigger context manager
```py
async def factory(c: Container):
    some_stuff = await c.aget(SomeOtherType)  # async factory because we want to do this
    return SomeType(some_stuff)

registry.register_factory(SomeType, factory)
```

Let's say `SomeType` is also context manager. The code won't enter/exit the context manager because [that only happens if the factory is not awaitable](https://github.com/hynek/svcs/blob/8ed3db3c8f742730a4d9e51a0d676b3c31423681/src/svcs/_core.py#L1008-L1015).


Discussion / hints:
I think that’s fixed in https://github.com/hynek/svcs/pull/93?
I think that's different. #93 was about entering non-async context managers, when `aget()` and sync factory is used.

This is about entering any context manager, when async _factory_ is used.
I think that’s fixed in https://github.com/hynek/svcs/pull/93?
I think that's different. #93 was about entering non-async context managers, when `aget()` and sync factory is used.

This is about entering any context manager, when async _factory_ is used.

Fix patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 49c9c62..0745321 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -40,6 +40,9 @@ You can find our backwards-compatibility policy [here](https://github.com/hynek/
 - `Container.aget()` now also enters and exits synchronous context managers.
   [#93](https://github.com/hynek/svcs/pull/93)
 
+- `Container.aget()` now also enters and exits context managers that are returned by async factories.
+  [#105](https://github.com/hynek/svcs/pull/105)
+
 
 ## [24.1.0](https://github.com/hynek/svcs/compare/23.21.0...24.1.0) - 2024-01-25
 
diff --git a/src/svcs/_core.py b/src/svcs/_core.py
index 4ed8a51..d90c595 100644
--- a/src/svcs/_core.py
+++ b/src/svcs/_core.py
@@ -1011,9 +1011,21 @@ async def aget(self, *svc_types: type) -> object:
             elif enter and isinstance(svc, AbstractContextManager):
                 self._on_close.append((name, svc))
                 svc = svc.__enter__()
+            # _lookup() doesn't handle async factories, so we have to live with
+            # some repetition.
             elif isawaitable(svc):
+                # Execute the factory. Until now, we've only created the
+                # awaitable.
                 svc = await svc
 
+                # Factory returned a contextmanager.
+                if enter and isinstance(svc, AbstractAsyncContextManager):
+                    self._on_close.append((name, svc))
+                    svc = await svc.__aenter__()
+                elif enter and isinstance(svc, AbstractContextManager):
+                    self._on_close.append((name, svc))
+                    svc = svc.__enter__()
+
             self._instantiated[svc_type] = svc
 
             rv.append(svc)


Test patch:
diff --git a/tests/test_container.py b/tests/test_container.py
index 4f9838b..12dda6c 100644
--- a/tests/test_container.py
+++ b/tests/test_container.py
@@ -4,6 +4,7 @@
 
 import gc
 
+from contextlib import asynccontextmanager, contextmanager
 from unittest.mock import Mock
 
 import pytest
@@ -91,6 +92,56 @@ async def factory():
 
         assert close_me.is_aclosed
 
+    @pytest.mark.asyncio
+    async def test_async_context_manager_sync_factory(self, container):
+        """
+        If an async factory returns an context manager, it's entered.
+        """
+        entered = left = False
+
+        @contextmanager
+        def cm():
+            nonlocal entered, left
+            entered = True
+            yield 42
+            left = True
+
+        async def factory(svcs_container):
+            return cm()
+
+        container.registry.register_factory(int, factory)
+
+        async with container:
+            assert 42 == await container.aget(int)
+
+        assert entered
+        assert left
+
+    @pytest.mark.asyncio
+    async def test_async_context_manager_async_factory(self, container):
+        """
+        If an async factory returns an async context manager, it's entered.
+        """
+        entered = left = False
+
+        @asynccontextmanager
+        async def cm():
+            nonlocal entered, left
+            entered = True
+            yield 42
+            left = True
+
+        async def factory(svcs_container):
+            return cm()
+
+        container.registry.register_factory(int, factory)
+
+        async with container:
+            assert 42 == await container.aget(int)
+
+        assert entered
+        assert left
+
     def test_gc_warning(self, recwarn, registry):
         """
         If a container is gc'ed with pending cleanups, a warning is raised.