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
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
Changelog
=========

[1.1.0](../../releases/tag/v1.1.0) - Unreleased
-----------------------------------------------

### Added

- option to add event handlers which accept no arguments

[1.0.0](../../releases/tag/v1.0.0) - 2022-03-13
-----------------------------------------------

Expand Down
2 changes: 1 addition & 1 deletion src/apify/_version.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = '1.0.0'
__version__ = '1.1.0'
27 changes: 20 additions & 7 deletions src/apify/event_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import inspect
import json
from collections import defaultdict
from typing import Any, Callable, Dict, List, Optional, Set
from typing import Any, Callable, Coroutine, Dict, List, Optional, Set, Union

import websockets.client
from pyee.asyncio import AsyncIOEventEmitter
Expand All @@ -12,6 +12,8 @@
from .consts import ActorEventTypes
from .log import logger

ListenerType = Union[Callable[[], None], Callable[[Any], None], Callable[[], Coroutine[Any, Any, None]], Callable[[Any], Coroutine[Any, Any, None]]]


@ignore_docs
class EventManager:
Expand Down Expand Up @@ -87,26 +89,37 @@ async def close(self, event_listeners_timeout_secs: Optional[float] = None) -> N

self._initialized = False

def on(self, event_name: ActorEventTypes, listener: Callable) -> Callable:
def on(self, event_name: ActorEventTypes, listener: ListenerType) -> Callable:
"""Add an event listener to the event manager.

Args:
event_name (ActorEventTypes): The actor event for which to listen to.
listener (Callable): The function which is to be called when the event is emitted (can be async).
Must accept either zero or one arguments (the first argument will be the event data).
"""
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!')

event_name = _maybe_extract_enum_member_value(event_name)

async def inner_wrapper(*args: Any, **kwargs: Any) -> None:
async def inner_wrapper(event_data: Any) -> None:
if inspect.iscoroutinefunction(listener):
await listener(*args, **kwargs)
if listener_argument_count == 0:
await listener()
else:
await listener(event_data)
else:
listener(*args, **kwargs)
if listener_argument_count == 0:
listener() # type: ignore[call-arg]
else:
listener(event_data) # type: ignore[call-arg]

async def outer_wrapper(*args: Any, **kwargs: Any) -> None:
listener_task = asyncio.create_task(inner_wrapper(*args, **kwargs))
async def outer_wrapper(event_data: Any) -> None:
listener_task = asyncio.create_task(inner_wrapper(event_data))
self._listener_tasks.add(listener_task)
try:
await listener_task
Expand Down
49 changes: 49 additions & 0 deletions tests/unit/test_event_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,55 @@ def event_handler(data: Any) -> None:

await event_manager.close()

async def test_event_handler_argument_counts_local(self) -> None:
config = Configuration()
event_manager = EventManager(config)

await event_manager.init()

event_calls = []

def sync_no_arguments() -> None:
nonlocal event_calls
event_calls.append(('sync_no_arguments', None))

async def async_no_arguments() -> None:
nonlocal event_calls
event_calls.append(('async_no_arguments', None))

def sync_one_argument(event_data: Any) -> None:
nonlocal event_calls
event_calls.append(('sync_one_argument', event_data))

async def async_one_argument(event_data: Any) -> None:
nonlocal event_calls
event_calls.append(('async_one_argument', event_data))

def sync_two_arguments(_arg1: Any, _arg2: Any) -> None:
pass

async def async_two_arguments(_arg1: Any, _arg2: Any) -> None:
pass

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)

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]
with pytest.raises(ValueError, match='The "listener" argument must be a callable which accepts 0 or 1 arguments!'):
event_manager.on(ActorEventTypes.SYSTEM_INFO, async_two_arguments) # type: ignore[arg-type]

event_manager.emit(ActorEventTypes.SYSTEM_INFO, 'DUMMY_SYSTEM_INFO')
await asyncio.sleep(0.1)

assert len(event_calls) == 4
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

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