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
21 changes: 18 additions & 3 deletions src/apify/event_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,9 +100,24 @@ def on(self, event_name: ActorEventTypes, listener: ListenerType) -> Callable:
if not self._initialized:
raise RuntimeError('EventManager was not initialized!')

listener_argument_count = len(inspect.signature(listener).parameters)
if listener_argument_count > 1:
raise ValueError('The "listener" argument must be a callable which accepts 0 or 1 arguments!')
# Detect whether the listener will accept the event_data argument
try:
signature = inspect.signature(listener)
except (ValueError, TypeError):
# If we can't determine the listener argument count (e.g. for the built-in `print` function),
# let's assume the listener will accept the argument
listener_argument_count = 1
else:
try:
dummy_event_data: Dict = {}
signature.bind(dummy_event_data)
listener_argument_count = 1
except TypeError:
try:
signature.bind()
listener_argument_count = 0
except TypeError:
raise ValueError('The "listener" argument must be a callable which accepts 0 or 1 arguments!')

event_name = _maybe_extract_enum_member_value(event_name)

Expand Down
21 changes: 20 additions & 1 deletion tests/unit/test_event_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import logging
import time
from collections import defaultdict
from pprint import pprint
from typing import Any, Callable, Dict, Optional, Set

import pytest
Expand Down Expand Up @@ -135,10 +136,26 @@ def sync_two_arguments(_arg1: Any, _arg2: Any) -> None:
async def async_two_arguments(_arg1: Any, _arg2: Any) -> None:
pass

def sync_two_arguments_one_default(event_data: Any, _arg2: Any = 'default_value') -> None:
nonlocal event_calls
event_calls.append(('sync_two_arguments_one_default', event_data))

async def async_two_arguments_one_default(event_data: Any, _arg2: Any = 'default_value') -> None:
nonlocal event_calls
event_calls.append(('async_two_arguments_one_default', event_data))

event_manager.on(ActorEventTypes.SYSTEM_INFO, sync_no_arguments)
event_manager.on(ActorEventTypes.SYSTEM_INFO, async_no_arguments)
event_manager.on(ActorEventTypes.SYSTEM_INFO, sync_one_argument)
event_manager.on(ActorEventTypes.SYSTEM_INFO, async_one_argument)
event_manager.on(ActorEventTypes.SYSTEM_INFO, sync_two_arguments_one_default)
event_manager.on(ActorEventTypes.SYSTEM_INFO, async_two_arguments_one_default)

# built-in functions should work too
event_manager.on(ActorEventTypes.SYSTEM_INFO, print)

# functions from the standard library should work too
event_manager.on(ActorEventTypes.SYSTEM_INFO, pprint)

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

assert len(event_calls) == 4
assert len(event_calls) == 6
assert ('sync_no_arguments', None) in event_calls
assert ('async_no_arguments', None) in event_calls
assert ('sync_one_argument', 'DUMMY_SYSTEM_INFO') in event_calls
assert ('async_one_argument', 'DUMMY_SYSTEM_INFO') in event_calls
assert ('sync_two_arguments_one_default', 'DUMMY_SYSTEM_INFO') in event_calls
assert ('async_two_arguments_one_default', 'DUMMY_SYSTEM_INFO') in event_calls

async def test_event_async_handling_local(self) -> None:
config = Configuration()
Expand Down