diff --git a/ovos_utils/events.py b/ovos_utils/events.py index 3663fd37..7e3426eb 100644 --- a/ovos_utils/events.py +++ b/ovos_utils/events.py @@ -1,7 +1,8 @@ + import time from datetime import datetime, timedelta from inspect import signature - +from typing import Callable, Optional from ovos_utils.intents.intent_service_interface import to_alnum from ovos_utils.log import LOG from ovos_utils.messagebus import FakeBus, FakeMessage as Message @@ -27,7 +28,7 @@ def unmunge_message(message: Message, skill_id: str) -> Message: return message -def get_handler_name(handler) -> str: +def get_handler_name(handler: Callable) -> str: """ Name (including class if available) of handler function. @@ -43,17 +44,20 @@ def get_handler_name(handler) -> str: return handler.__name__ -def create_wrapper(handler, skill_id, on_start, on_end, on_error) -> callable: +def create_wrapper(handler: Callable, skill_id: str, + on_start: Callable, on_end: Callable, + on_error: Callable) -> Callable: """ Create the default skill handler wrapper. - This wrapper handles things like metrics, reporting handler start/stop and errors. - handler (callable): method/function to call - skill_id: skill_id for associated skill - on_start (function): function to call before executing the handler - on_end (function): function to call after executing the handler - on_error (function): function to call for error reporting + + @param handler: method/function to call + @param skill_id: skill_id for associated skill + @param on_start: function to call before executing the handler + @param on_end: function to call after executing the handler + @param on_error: function to call for error reporting + @return: callable implementing the passed methods """ def wrapper(message): @@ -80,8 +84,10 @@ def wrapper(message): return wrapper -def create_basic_wrapper(handler, on_error=None) -> callable: - """Create the default skill handler wrapper. +def create_basic_wrapper(handler: Callable, + on_error: Optional[Callable] = None) -> Callable: + """ + Create the default skill handler wrapper. This wrapper handles things like metrics, reporting handler start/stop and errors. @@ -108,7 +114,8 @@ def wrapper(message): class EventContainer: - """Container tracking messagbus handlers. + """ + Container tracking messagebus handlers. This container tracks events added by a skill, allowing unregistering all events on shutdown. @@ -121,14 +128,14 @@ def __init__(self, bus=None): def set_bus(self, bus): self.bus = bus - def add(self, name, handler, once=False): - """Create event handler for executing intent or other event. + def add(self, name: str, handler: Callable, once: bool = False): + """ + Create event handler for executing intent or other event. Arguments: - name (string): IntentParser name - handler (func): Method to call - once (bool, optional): Event handler will be removed after it has - been run once. + name: IntentParser name + handler: Method to call + once: Event handler will be removed after it has been run once. """ def once_wrapper(message): @@ -147,8 +154,9 @@ def once_wrapper(message): LOG.debug('Added event: {}'.format(name)) - def remove(self, name): - """Removes an event from bus emitter and events list. + def remove(self, name: str) -> bool: + """ + Removes an event from bus emitter and events list. Args: name (string): Name of Intent or Scheduler Event @@ -181,7 +189,8 @@ def __iter__(self): return iter(self.events) def clear(self): - """Unregister all registered handlers and clear the list of registered + """ + Unregister all registered handlers and clear the list of registered events. """ for e, f in self.events: @@ -193,7 +202,8 @@ class EventSchedulerInterface: """Interface for accessing the event scheduler over the message bus.""" def __init__(self, name=None, sched_id=None, bus=None, skill_id=None): - # NOTE: can not rename or move sched_id/name arguments to keep api compatibility + # NOTE: can not rename or move sched_id/name arguments to keep api + # compatibility if name: LOG.warning("name argument has been deprecated! use skill_id instead") if sched_id: @@ -252,7 +262,10 @@ def _schedule_event(self, handler, when, data, name, context (dict, optional): message context to send when the handler is called """ - if isinstance(when, (int, float)) and when >= 0: + if isinstance(when, (int, float)): + if when < 0: + raise ValueError(f"Expected datetime or positive int/float. " + f"got: {when}") when = datetime.now() + timedelta(seconds=when) if not name: name = self.skill_id + handler.__name__ @@ -267,7 +280,7 @@ def on_error(e): wrapped = create_basic_wrapper(handler, on_error) self.events.add(unique_name, wrapped, once=not repeat_interval) - event_data = {'time': time.mktime(when.timetuple()), + event_data = {'time': when.timestamp(), 'event': unique_name, 'repeat': repeat_interval, 'data': data} diff --git a/test/unittests/test_events.py b/test/unittests/test_events.py index 6b374457..275a605d 100644 --- a/test/unittests/test_events.py +++ b/test/unittests/test_events.py @@ -1,14 +1,45 @@ import unittest +import datetime + +from os.path import join, dirname +from threading import Event +from time import time +from unittest.mock import Mock + +from ovos_utils.messagebus import FakeBus, Message class TestEvents(unittest.TestCase): + bus = FakeBus() + test_schedule = join(dirname(__file__), "schedule.json") + def test_unmunge_message(self): from ovos_utils.events import unmunge_message - # TODO + test_message = Message("test", {"TESTSKILLTESTSKILL": True, + "TESTSKILLdata": "nothing"}) + self.assertEqual(unmunge_message(test_message, "OtherSkill"), + test_message) + unmunged = unmunge_message(test_message, "TESTSKILL") + self.assertEqual(unmunged.msg_type, test_message.msg_type) + self.assertEqual(unmunged.data, {"TESTSKILL": True, + "data": "nothing"}) def test_get_handler_name(self): from ovos_utils.events import get_handler_name - # TODO + + class Test: + def __init__(self): + self.name = "test" + + def handler(self, msg): + print(f"{self.name}: {msg}") + + self.assertEqual(get_handler_name(Test().handler), "test.handler") + + def handler(): + print("") + + self.assertEqual(get_handler_name(handler), "handler") def test_create_wrapper(self): from ovos_utils.events import create_wrapper @@ -23,5 +54,78 @@ def test_event_container(self): # TODO def test_event_scheduler_interface(self): - from ovos_utils.events import EventSchedulerInterface - # TODO + from ovos_utils.events import EventSchedulerInterface, EventContainer + interface = EventSchedulerInterface(bus=self.bus, name="test") + self.assertEqual(interface.bus, self.bus) + self.assertIsInstance(interface.skill_id, str) + test_id = "testing" + interface.set_id(test_id) + self.assertEqual(interface.skill_id, test_id) + self.assertIsInstance(interface.events, EventContainer) + self.assertEqual(interface.events.bus, self.bus) + self.assertEqual(interface.scheduled_repeats, list()) + + now_time = datetime.datetime.now(datetime.timezone.utc) + self.assertAlmostEqual(now_time.timestamp(), time(), 0) + event_time_tzaware = now_time + datetime.timedelta(hours=1) + event_time_seconds = event_time_tzaware.timestamp() + event_time_tznaive = datetime.datetime.fromtimestamp(event_time_seconds) + + scheduled = Event() + messages = list() + + def on_schedule(msg): + nonlocal messages + messages.append(msg) + scheduled.set() + + self.bus.on('mycroft.scheduler.schedule_event', on_schedule) + + context = { + "test": time() + } + + data = { + "test": True + } + + callback = Mock() + callback.__name__ = "test" + + # Schedule TZ Aware + scheduled.clear() + interface.schedule_event(callback, event_time_tzaware, data, + context=context) + self.assertTrue(scheduled.wait(2)) + self.assertEqual(len(messages), 1) + + # Schedule TZ Naive + scheduled.clear() + interface.schedule_event(callback, event_time_tznaive, data, + context=context) + self.assertTrue(scheduled.wait(2)) + self.assertEqual(len(messages), 2) + + # Schedule duration + interface.schedule_event(callback, event_time_seconds - + datetime.datetime.now().timestamp(), + data, context=context) + self.assertTrue(scheduled.wait(2)) + self.assertEqual(len(messages), 3) + + for event in messages: + self.assertIsInstance(event, Message) + self.assertEqual(event.context, context) + self.assertEqual(event.data['data'], data) + self.assertIsInstance(event.data['event'], str) + self.assertIsNone(event.data['repeat']) + self.assertAlmostEqual(event.data['time'], event_time_seconds, 0) + + # Schedule invalid + with self.assertRaises(ValueError): + interface.schedule_event(callback, -3.0) + + # TODO: Test Repeating, Update, Cancel, Get Status + + interface.shutdown() + self.assertEqual(interface.events.events, list())