Skip to content

Commit 93fc7c8

Browse files
authored
Fix detecting signature of built-in functions in event handler (#86)
I found out that `inspect.signature()` cannot read the signature of built-in methods, so e.g. `Actor.on(ActorEventTypes.SYSTEM_INFO, print)` would not work. This fixes it by rewriting the signature-processing to ignore errors when the signature can't be read, and also to better check methods which have default arguments, by just trying to bind the arguments to the method signature.
1 parent d1a17db commit 93fc7c8

File tree

2 files changed

+38
-4
lines changed

2 files changed

+38
-4
lines changed

src/apify/event_manager.py

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -100,9 +100,24 @@ def on(self, event_name: ActorEventTypes, listener: ListenerType) -> Callable:
100100
if not self._initialized:
101101
raise RuntimeError('EventManager was not initialized!')
102102

103-
listener_argument_count = len(inspect.signature(listener).parameters)
104-
if listener_argument_count > 1:
105-
raise ValueError('The "listener" argument must be a callable which accepts 0 or 1 arguments!')
103+
# Detect whether the listener will accept the event_data argument
104+
try:
105+
signature = inspect.signature(listener)
106+
except (ValueError, TypeError):
107+
# If we can't determine the listener argument count (e.g. for the built-in `print` function),
108+
# let's assume the listener will accept the argument
109+
listener_argument_count = 1
110+
else:
111+
try:
112+
dummy_event_data: Dict = {}
113+
signature.bind(dummy_event_data)
114+
listener_argument_count = 1
115+
except TypeError:
116+
try:
117+
signature.bind()
118+
listener_argument_count = 0
119+
except TypeError:
120+
raise ValueError('The "listener" argument must be a callable which accepts 0 or 1 arguments!')
106121

107122
event_name = _maybe_extract_enum_member_value(event_name)
108123

tests/unit/test_event_manager.py

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import logging
44
import time
55
from collections import defaultdict
6+
from pprint import pprint
67
from typing import Any, Callable, Dict, Optional, Set
78

89
import pytest
@@ -135,10 +136,26 @@ def sync_two_arguments(_arg1: Any, _arg2: Any) -> None:
135136
async def async_two_arguments(_arg1: Any, _arg2: Any) -> None:
136137
pass
137138

139+
def sync_two_arguments_one_default(event_data: Any, _arg2: Any = 'default_value') -> None:
140+
nonlocal event_calls
141+
event_calls.append(('sync_two_arguments_one_default', event_data))
142+
143+
async def async_two_arguments_one_default(event_data: Any, _arg2: Any = 'default_value') -> None:
144+
nonlocal event_calls
145+
event_calls.append(('async_two_arguments_one_default', event_data))
146+
138147
event_manager.on(ActorEventTypes.SYSTEM_INFO, sync_no_arguments)
139148
event_manager.on(ActorEventTypes.SYSTEM_INFO, async_no_arguments)
140149
event_manager.on(ActorEventTypes.SYSTEM_INFO, sync_one_argument)
141150
event_manager.on(ActorEventTypes.SYSTEM_INFO, async_one_argument)
151+
event_manager.on(ActorEventTypes.SYSTEM_INFO, sync_two_arguments_one_default)
152+
event_manager.on(ActorEventTypes.SYSTEM_INFO, async_two_arguments_one_default)
153+
154+
# built-in functions should work too
155+
event_manager.on(ActorEventTypes.SYSTEM_INFO, print)
156+
157+
# functions from the standard library should work too
158+
event_manager.on(ActorEventTypes.SYSTEM_INFO, pprint)
142159

143160
with pytest.raises(ValueError, match='The "listener" argument must be a callable which accepts 0 or 1 arguments!'):
144161
event_manager.on(ActorEventTypes.SYSTEM_INFO, sync_two_arguments) # type: ignore[arg-type]
@@ -148,11 +165,13 @@ async def async_two_arguments(_arg1: Any, _arg2: Any) -> None:
148165
event_manager.emit(ActorEventTypes.SYSTEM_INFO, 'DUMMY_SYSTEM_INFO')
149166
await asyncio.sleep(0.1)
150167

151-
assert len(event_calls) == 4
168+
assert len(event_calls) == 6
152169
assert ('sync_no_arguments', None) in event_calls
153170
assert ('async_no_arguments', None) in event_calls
154171
assert ('sync_one_argument', 'DUMMY_SYSTEM_INFO') in event_calls
155172
assert ('async_one_argument', 'DUMMY_SYSTEM_INFO') in event_calls
173+
assert ('sync_two_arguments_one_default', 'DUMMY_SYSTEM_INFO') in event_calls
174+
assert ('async_two_arguments_one_default', 'DUMMY_SYSTEM_INFO') in event_calls
156175

157176
async def test_event_async_handling_local(self) -> None:
158177
config = Configuration()

0 commit comments

Comments
 (0)