Repository: ivankorobkov/python-inject

Issue:
inspect.iscoroutinefunction fails after inject wraps
Decorating functions with param/params/autoparams returns a synchronous function instead of preserving the async function. The following inspect.iscoroutinefunction will fail. This causes problems when other decorators depend on inspecting the function prior to executing it. 

```python
@inject.param("val")
async def test_func(val):
    return val

assert inspect.iscoroutinefunction(test_func)
```

Discussion:

Patch:
diff --git a/.gitignore b/.gitignore
index 7a0d4c2..106aa6c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,10 +1,12 @@
 # Intellij Idea
 .idea/
 *.iml
+.vscode/
 
 # Byte-compiled / optimized / DLL files
 __pycache__/
 *.py[cod]
+.mypy_cache/
 
 # C extensions
 *.so
@@ -57,3 +59,6 @@ docs/_build/
 
 # macOS files
 .DS_Store
+
+# pyenv virtualenv
+.python-version
\ No newline at end of file
diff --git a/inject/__init__.py b/inject/__init__.py
index d857b9b..d32af79 100644
--- a/inject/__init__.py
+++ b/inject/__init__.py
@@ -86,7 +86,7 @@ import threading
 import types
 from functools import wraps
 from typing import (Any, Callable, Dict, Generic, Hashable, Optional, Type,
-                    TypeVar, Union, get_type_hints, overload)
+                    TypeVar, Union, get_type_hints, overload, Awaitable, cast)
 
 _NEW_TYPING = sys.version_info[:3] >= (3, 7, 0)  # PEP 560
 _RETURN = 'return'
@@ -280,12 +280,22 @@ class _ParameterInjection(Generic[T]):
         self._name = name
         self._cls = cls
 
-    def __call__(self, func: Callable[..., T]) -> Callable[..., T]:
+    def __call__(self, func: Callable[..., Union[T, Awaitable[T]]]) -> Callable[..., Union[T, Awaitable[T]]]:
+        if inspect.iscoroutinefunction(func):
+            @wraps(func)
+            async def async_injection_wrapper(*args: Any, **kwargs: Any) -> T:
+                if self._name not in kwargs:
+                    kwargs[self._name] = instance(self._cls or self._name)
+                async_func = cast(Callable[..., Awaitable[T]], func)
+                return await async_func(*args, **kwargs)
+            return async_injection_wrapper
+        
         @wraps(func)
         def injection_wrapper(*args: Any, **kwargs: Any) -> T:
             if self._name not in kwargs:
                 kwargs[self._name] = instance(self._cls or self._name)
-            return func(*args, **kwargs)
+            sync_func = cast(Callable[..., T], func)
+            return sync_func(*args, **kwargs)
 
         return injection_wrapper
 
@@ -296,13 +306,29 @@ class _ParametersInjection(Generic[T]):
     def __init__(self, **kwargs: Any) -> None:
         self._params = kwargs
 
-    def __call__(self, func: Callable[..., T]) -> Callable[..., T]:
+    def __call__(self, func: Callable[..., Union[Awaitable[T], T]]) -> Callable[..., Union[Awaitable[T], T]]:
         if sys.version_info.major == 2:
             arg_names = inspect.getargspec(func).args
         else:
             arg_names = inspect.getfullargspec(func).args
         params_to_provide = self._params
 
+        if inspect.iscoroutinefunction(func):
+            @wraps(func)
+            async def async_injection_wrapper(*args: Any, **kwargs: Any) -> T:
+                provided_params = frozenset(
+                    arg_names[:len(args)]) | frozenset(kwargs.keys())
+                for param, cls in params_to_provide.items():
+                    if param not in provided_params:
+                        kwargs[param] = instance(cls)
+                async_func = cast(Callable[..., Awaitable[T]], func)
+                try:
+                    return await async_func(*args, **kwargs)
+                except TypeError as previous_error:
+                    raise ConstructorTypeError(func, previous_error)
+
+            return async_injection_wrapper
+
         @wraps(func)
         def injection_wrapper(*args: Any, **kwargs: Any) -> T:
             provided_params = frozenset(
@@ -310,8 +336,9 @@ class _ParametersInjection(Generic[T]):
             for param, cls in params_to_provide.items():
                 if param not in provided_params:
                     kwargs[param] = instance(cls)
+            sync_func = cast(Callable[..., T], func)
             try:
-                return func(*args, **kwargs)
+                return sync_func(*args, **kwargs)
             except TypeError as previous_error:
                 raise ConstructorTypeError(func, previous_error)
         return injection_wrapper


Test patch:
diff --git a/test/__init__.py b/test/__init__.py
index 9d81dad..d4e0635 100644
--- a/test/__init__.py
+++ b/test/__init__.py
@@ -1,8 +1,12 @@
 from unittest import TestCase
-
+import asyncio
 import inject
 
 
 class BaseTestInject(TestCase):
     def tearDown(self):
-        inject.clear()
\ No newline at end of file
+        inject.clear()
+    
+    def run_async(self, awaitable):
+        loop = asyncio.get_event_loop()
+        return loop.run_until_complete(awaitable)
\ No newline at end of file
diff --git a/test/test_param.py b/test/test_param.py
index 452eafc..a21bde4 100644
--- a/test/test_param.py
+++ b/test/test_param.py
@@ -1,6 +1,7 @@
 import inject
 from test import BaseTestInject
-
+import inspect
+import asyncio
 
 class TestInjectParams(BaseTestInject):
     def test_param_by_name(self):
@@ -21,3 +22,13 @@ class TestInjectParams(BaseTestInject):
         inject.configure(lambda binder: binder.bind(int, 123))
 
         assert test_func() == 123
+    
+    def test_async_param(self):
+        @inject.param('val')
+        async def test_func(val):
+            return val
+        
+        inject.configure(lambda binder: binder.bind('val', 123))
+
+        assert inspect.iscoroutinefunction(test_func)
+        assert self.run_async(test_func()) == 123
diff --git a/test/test_params.py b/test/test_params.py
index 543ef27..fc2e8dc 100644
--- a/test/test_params.py
+++ b/test/test_params.py
@@ -1,6 +1,7 @@
 import inject
 from test import BaseTestInject
-
+import inspect
+import asyncio
 
 class TestInjectParams(BaseTestInject):
     def test_params(self):
@@ -134,3 +135,15 @@ class TestInjectParams(BaseTestInject):
         assert test.func(a=10, c=30) == (Test, 10, 2, 30)
         assert test.func(c=30, b=20, a=10) == (Test, 10, 20, 30)
         assert test.func(10, b=20) == (Test, 10, 20, 3)
+
+    def test_async_params(self):
+        @inject.params(val=int)
+        async def test_func(val):
+            return val
+
+        inject.configure(lambda binder: binder.bind(int, 123))
+
+        assert inspect.iscoroutinefunction(test_func)
+        assert self.run_async(test_func()) == 123
+        assert self.run_async(test_func(321)) == 321
+        assert self.run_async(test_func(val=42)) == 42