Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ maintainers = [{name = "Vadim Kozyrevskiy", email = "vadikko2@mail.ru"}]
name = "python-cqrs"
readme = "README.md"
requires-python = ">=3.10"
version = "4.6.1"
version = "4.6.2"

[project.optional-dependencies]
aiobreaker = ["aiobreaker>=0.3.0"]
Expand Down
8 changes: 7 additions & 1 deletion src/cqrs/container/dependency_injector.py
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,13 @@ async def resolve(self, type_: type[T]) -> T:
... return await service.create_user(request.name)
"""
provider = self._get_provider(type_)
return provider()
result = provider()
# If provider returns a coroutine or Future (async provider), await it
# Note: inspect.iscoroutine() only checks for coroutines, not Futures/Tasks
# We need to check for any awaitable object
if inspect.isawaitable(result):
return await result
return result

def _traverse_container(
self,
Expand Down
54 changes: 53 additions & 1 deletion tests/unit/test_dependency_injector_cqrs_container.py
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,7 @@ async def test_override_provider_with_limited_support(self) -> None:
providers.Factory(
OverriddenUserService,
repository=container.user_repository,
)
),
)

# Works: Resolve via abstract interface (inheritance match + override)
Expand Down Expand Up @@ -277,3 +277,55 @@ async def test_multiple_container_attachment(self) -> None:
# Previous container's types are no longer resolvable
with pytest.raises(ValueError):
await cqrs_container.resolve(UserRepository)

async def test_resolve_async_provider_returns_future(self) -> None:
"""
Resolution correctly handles providers that return Future objects.

This test validates the fix for the bug where providers returning Future
objects (instead of coroutines) were not being awaited, causing
AttributeError: '_asyncio.Future' object has no attribute 'handle'.

The issue occurred when:
1. A provider returns a Future (not a coroutine)
2. inspect.iscoroutine() returns False for Future objects
3. The Future is returned directly without being awaited
4. Downstream code tries to call .handle() on the Future, causing an error

The fix uses inspect.isawaitable() instead of inspect.iscoroutine() to
properly detect and await Future objects.
"""
import asyncio

class AsyncService:
def __init__(self) -> None:
self.initialized = True

async def do_work(self) -> str:
return "work done"

# Create a provider that returns a coroutine/Future
# This simulates the scenario where dependency-injector creates
# a Future instead of a coroutine
async def async_factory() -> AsyncService:
await asyncio.sleep(0.01) # Simulate async initialization
return AsyncService()

class AsyncContainer(containers.DeclarativeContainer):
# Use Factory provider with async factory function
# This properly registers AsyncService type in the container
async_service = providers.Factory(
AsyncService,
)

cqrs_container = DependencyInjectorCQRSContainer()
container = AsyncContainer()
cqrs_container.attach_external_container(container)

# This should work: the provider returns AsyncService instance
service = await cqrs_container.resolve(AsyncService)

assert isinstance(service, AsyncService)
assert service.initialized is True
result = await service.do_work()
assert result == "work done"
Loading