diff --git a/CHANGELOG.md b/CHANGELOG.md index a737698f..1689fb04 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 ----------------------------------------------- diff --git a/src/apify/_version.py b/src/apify/_version.py index 1f356cc5..1a72d32e 100644 --- a/src/apify/_version.py +++ b/src/apify/_version.py @@ -1 +1 @@ -__version__ = '1.0.0' +__version__ = '1.1.0' diff --git a/src/apify/event_manager.py b/src/apify/event_manager.py index 3dc0bf18..4241c071 100644 --- a/src/apify/event_manager.py +++ b/src/apify/event_manager.py @@ -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 @@ -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: @@ -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 diff --git a/tests/unit/test_event_manager.py b/tests/unit/test_event_manager.py index 632552a0..11dcd3c1 100644 --- a/tests/unit/test_event_manager.py +++ b/tests/unit/test_event_manager.py @@ -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)