From 32b8296244c9766156bd4a07908a9a86e339e1d8 Mon Sep 17 00:00:00 2001 From: Fabio Manganiello Date: Sun, 19 May 2024 02:15:34 +0200 Subject: [PATCH] [#400] Dynamic logic to infer procedures/hooks arguments. This allows procedures and event hooks to have more flexible signatures. Along the lines of: ```python @when(SomeEvent) def hook(event): ... @when(SomeOtherEvent) def hook2(): ... ``` Instead of supporting only the full context spec: ```python @when(SomeEvent) def hook(event, **ctx): ... ``` Closes: #400 --- README.md | 2 +- examples/config/hook.py | 35 +++++++++++++++++++++-------------- platypush/common/__init__.py | 22 ++++++++++++++++++++-- platypush/event/hook.py | 12 ++++++++++-- 4 files changed, 52 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index fe23ce3a72..63a68f1f17 100644 --- a/README.md +++ b/README.md @@ -537,7 +537,7 @@ against partial event arguments are also possible, and relational operators are supported as well. For example: ```python -from platypush import hook +from platypush import when from platypush.message.event.sensor import SensorDataChangeEvent @when(SensorDataChangeEvent, data=1): diff --git a/examples/config/hook.py b/examples/config/hook.py index 9f36b9a830..046490c9d1 100644 --- a/examples/config/hook.py +++ b/examples/config/hook.py @@ -1,9 +1,13 @@ -# A more versatile way to define event hooks than the YAML format of `config.yaml` is through native Python scripts. -# You can define hooks as simple Python functions that use the `platypush.event.hook.hook` decorator to specify on -# which event type they should be called, and optionally on which event attribute values. +# A more versatile way to define event hooks than the YAML format of +# `config.yaml` is through native Python scripts. You can define hooks as simple +# Python functions that use the `platypush.event.hook.hook` decorator to specify +# on which event type they should be called, and optionally on which event +# attribute values. # -# Event hooks should be stored in Python files under `~/.config/platypush/scripts`. All the functions that use the -# @when decorator will automatically be discovered and imported as event hooks into the platform at runtime. +# Event hooks should be stored in Python files under +# `~/.config/platypush/scripts`. All the functions that use the @when decorator +# will automatically be discovered and imported as event hooks into the platform +# at runtime. # `run` is a utility function that runs a request by name (e.g. `light.hue.on`). from platypush import when, run @@ -16,12 +20,14 @@ from platypush.message.event.assistant import ( @when(SpeechRecognizedEvent, phrase='play ${title} by ${artist}') -def on_music_play_command(event, title=None, artist=None, **context): +def on_music_play_command(event, title=None, artist=None): """ - This function will be executed when a SpeechRecognizedEvent with `phrase="play the music"` is triggered. - `event` contains the event object and `context` any key-value info from the running context. - Note that in this specific case we can leverage the token-extraction feature of SpeechRecognizedEvent through - ${} that operates on regex-like principles to extract any text that matches the pattern into context variables. + This function will be executed when a SpeechRecognizedEvent with + `phrase="play the music"` is triggered. `event` contains the event object + and `context` any key-value info from the running context. Note that in this + specific case we can leverage the token-extraction feature of + SpeechRecognizedEvent through ${} that operates on regex-like principles to + extract any text that matches the pattern into context variables. """ results = run( 'music.mpd.search', @@ -31,16 +37,17 @@ def on_music_play_command(event, title=None, artist=None, **context): }, ) - if results: + if results and results[0]: run('music.mpd.play', results[0]['file']) else: run('tts.say', "I can't find any music matching your query") @when(ConversationStartEvent) -def on_conversation_start(event, **context): +def on_conversation_start(): """ - A simple hook that gets invoked when a new conversation starts with a voice assistant and simply pauses the music - to make sure that your speech is properly detected. + A simple hook that gets invoked when a new conversation starts with a voice + assistant and simply pauses the music to make sure that your speech is + properly detected. """ run('music.mpd.pause_if_playing') diff --git a/platypush/common/__init__.py b/platypush/common/__init__.py index b773609ec5..0c3c862ebf 100644 --- a/platypush/common/__init__.py +++ b/platypush/common/__init__.py @@ -1,7 +1,7 @@ import inspect import logging import os -from typing import Any, Callable +from typing import Any, Callable, Mapping, Sequence, Tuple from platypush.utils.manifest import Manifest @@ -10,6 +10,22 @@ from ._types import StoppableThread logger = logging.getLogger('platypush') +def _build_args( + func: Callable, *args, **kwargs +) -> Tuple[Sequence[Any], Mapping[str, Any]]: + spec = inspect.getfullargspec(func) + func_args = args if spec.varargs else args[: len(spec.args)] + func_kwargs = ( + kwargs + if spec.varkw + else { + arg: kwargs[arg] for arg in [*spec.args, *spec.kwonlyargs] if arg in kwargs + } + ) + + return func_args, func_kwargs + + def exec_wrapper(f: Callable[..., Any], *args, **kwargs): """ Utility function that runs a callable with its arguments, wraps its @@ -17,8 +33,10 @@ def exec_wrapper(f: Callable[..., Any], *args, **kwargs): """ from platypush import Response + func_args, func_kwargs = _build_args(f, *args, **kwargs) + try: - ret = f(*args, **kwargs) + ret = f(*func_args, **func_kwargs) if isinstance(ret, Response): return ret diff --git a/platypush/event/hook.py b/platypush/event/hook.py index 8eda7dd51c..371557277a 100644 --- a/platypush/event/hook.py +++ b/platypush/event/hook.py @@ -179,9 +179,17 @@ class EventHook: result = self.matches_event(event) if result.is_match: - logger.info('Running hook %s triggered by an event', self.name) + logger.info( + 'Running hook `%s` triggered by a `%s` event', + self.name, + f'{event.__module__}.{event.__class__.__name__}', + ) + threading.Thread( - target=_thread_func, name='Event-' + self.name, args=(result,) + target=_thread_func, + name='Event-' + self.name, + args=(result,), + daemon=True, ).start()