Repository: hikari-py/hikari
Pull request #687 (2021-06-30 14:52:13)

Only resolve type hints in @listen when necessary
### Summary
Only resolve type hints in @listen when necessary
(This avoids errors being raised by reflect.resolve_signature when the event type was explicitly provided)
* Allow for listeners with other defaulted arguments

### Checklist
<!-- Make sure to tick all the following boxes by putting an `x` in between (like this `[x]`) -->
- [x] I have run `nox` and all the pipelines have passed.
- [x] I have made unittests according to the code I have added/modified/deleted.

### Related issues
<!--
To mention an issue use `#issue-id` and to mention a merge request use `!merge-request-id`
To close/fix an issue use `Close #issue-id` or `Fix #issue-id` (depending on the merge request)
-->



Patch:
diff --git a/hikari/impl/event_manager_base.py b/hikari/impl/event_manager_base.py
index 616cdbfb..5ad0e680 100644
--- a/hikari/impl/event_manager_base.py
+++ b/hikari/impl/event_manager_base.py
@@ -229,6 +229,14 @@ def _default_predicate(_: event_manager_.EventT_inv) -> bool:
     return True
 
 
+def _assert_is_listener(parameters: typing.Iterator[inspect.Parameter], /) -> None:
+    if next(parameters, None) is None:
+        raise TypeError("Event listener must have one positional argument for the event object.")
+
+    if any(param.default is not inspect.Parameter.empty for param in parameters):
+        raise TypeError("Only the first argument for a listener can be required, the event argument.")
+
+
 class EventManagerBase(event_manager_.EventManager):
     """Provides functionality to consume and dispatch events.
 
@@ -354,15 +362,17 @@ def decorator(
         ) -> event_manager_.CallbackT[event_manager_.EventT_co]:
             nonlocal event_type
 
-            signature = reflect.resolve_signature(callback)
-            params = signature.parameters.values()
+            # Avoid resolving forward references in the function's signature if
+            # event_type was explicitly provided as this may lead to errors.
+            if event_type is not None:
+                _assert_is_listener(iter(inspect.signature(callback).parameters.values()))
 
-            if len(params) != 1:
-                raise TypeError("Event listener must have exactly one parameter, the event object.")
-
-            event_param = next(iter(params))
+            else:
+                signature = reflect.resolve_signature(callback)
+                params = signature.parameters.values()
+                _assert_is_listener(iter(params))
+                event_param = next(iter(params))
 
-            if event_type is None:
                 if event_param.annotation is event_param.empty:
                     raise TypeError("Must provide the event type in the @listen decorator or as a type hint!")
 


Test patch:
diff --git a/tests/hikari/impl/test_event_manager_base.py b/tests/hikari/impl/test_event_manager_base.py
index 57fdd7ae..8ff38beb 100644
--- a/tests/hikari/impl/test_event_manager_base.py
+++ b/tests/hikari/impl/test_event_manager_base.py
@@ -20,6 +20,7 @@
 # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 # SOFTWARE.
 import asyncio
+import contextlib
 import logging
 import unittest
 import warnings
@@ -34,6 +35,7 @@
 from hikari.events import base_events
 from hikari.events import member_events
 from hikari.impl import event_manager_base
+from hikari.internal import reflect
 from tests.hikari import hikari_test_helpers
 
 
@@ -660,13 +662,20 @@ def test_listen_when_no_params(self, event_manager):
             async def test():
                 ...
 
-    def test_listen_when_more_then_one_param(self, event_manager):
+    def test_listen_when_more_then_one_param_when_provided_in_typehint(self, event_manager):
         with pytest.raises(TypeError):
 
             @event_manager.listen()
             async def test(a, b, c):
                 ...
 
+    def test_listen_when_more_then_one_param_when_provided_in_decorator(self, event_manager):
+        with pytest.raises(TypeError):
+
+            @event_manager.listen(object)
+            async def test(a, b, c):
+                ...
+
     def test_listen_when_param_not_provided_in_decorator_nor_typehint(self, event_manager):
         with pytest.raises(TypeError):
 
@@ -675,12 +684,18 @@ async def test(event):
                 ...
 
     def test_listen_when_param_provided_in_decorator(self, event_manager):
-        with mock.patch.object(event_manager_base.EventManagerBase, "subscribe") as subscribe:
+        stack = contextlib.ExitStack()
+
+        subscribe = stack.enter_context(mock.patch.object(event_manager_base.EventManagerBase, "subscribe"))
+        resolve_signature = stack.enter_context(mock.patch.object(reflect, "resolve_signature"))
+
+        with stack:
 
             @event_manager.listen(member_events.MemberCreateEvent)
             async def test(event):
                 ...
 
+        resolve_signature.assert_not_called()
         subscribe.assert_called_once_with(member_events.MemberCreateEvent, test, _nested=1)
 
     def test_listen_when_param_provided_in_typehint(self, event_manager):