[#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
This commit is contained in:
Fabio Manganiello 2024-05-19 02:15:34 +02:00
parent 2ab1743bec
commit 32b8296244
Signed by: blacklight
GPG key ID: D90FBA7F76362774
4 changed files with 52 additions and 19 deletions

View file

@ -537,7 +537,7 @@ against partial event arguments are also possible, and relational operators are
supported as well. For example: supported as well. For example:
```python ```python
from platypush import hook from platypush import when
from platypush.message.event.sensor import SensorDataChangeEvent from platypush.message.event.sensor import SensorDataChangeEvent
@when(SensorDataChangeEvent, data=1): @when(SensorDataChangeEvent, data=1):

View file

@ -1,9 +1,13 @@
# A more versatile way to define event hooks than the YAML format of `config.yaml` is through native Python scripts. # A more versatile way to define event hooks than the YAML format of
# You can define hooks as simple Python functions that use the `platypush.event.hook.hook` decorator to specify on # `config.yaml` is through native Python scripts. You can define hooks as simple
# which event type they should be called, and optionally on which event attribute values. # 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 # Event hooks should be stored in Python files under
# @when decorator will automatically be discovered and imported as event hooks into the platform at runtime. # `~/.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`). # `run` is a utility function that runs a request by name (e.g. `light.hue.on`).
from platypush import when, run from platypush import when, run
@ -16,12 +20,14 @@ from platypush.message.event.assistant import (
@when(SpeechRecognizedEvent, phrase='play ${title} by ${artist}') @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. This function will be executed when a SpeechRecognizedEvent with
`event` contains the event object and `context` any key-value info from the running context. `phrase="play the music"` is triggered. `event` contains the event object
Note that in this specific case we can leverage the token-extraction feature of SpeechRecognizedEvent through and `context` any key-value info from the running context. Note that in this
${} that operates on regex-like principles to extract any text that matches the pattern into context variables. 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( results = run(
'music.mpd.search', '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']) run('music.mpd.play', results[0]['file'])
else: else:
run('tts.say', "I can't find any music matching your query") run('tts.say', "I can't find any music matching your query")
@when(ConversationStartEvent) @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 A simple hook that gets invoked when a new conversation starts with a voice
to make sure that your speech is properly detected. assistant and simply pauses the music to make sure that your speech is
properly detected.
""" """
run('music.mpd.pause_if_playing') run('music.mpd.pause_if_playing')

View file

@ -1,7 +1,7 @@
import inspect import inspect
import logging import logging
import os import os
from typing import Any, Callable from typing import Any, Callable, Mapping, Sequence, Tuple
from platypush.utils.manifest import Manifest from platypush.utils.manifest import Manifest
@ -10,6 +10,22 @@ from ._types import StoppableThread
logger = logging.getLogger('platypush') 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): def exec_wrapper(f: Callable[..., Any], *args, **kwargs):
""" """
Utility function that runs a callable with its arguments, wraps its 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 from platypush import Response
func_args, func_kwargs = _build_args(f, *args, **kwargs)
try: try:
ret = f(*args, **kwargs) ret = f(*func_args, **func_kwargs)
if isinstance(ret, Response): if isinstance(ret, Response):
return ret return ret

View file

@ -179,9 +179,17 @@ class EventHook:
result = self.matches_event(event) result = self.matches_event(event)
if result.is_match: 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( threading.Thread(
target=_thread_func, name='Event-' + self.name, args=(result,) target=_thread_func,
name='Event-' + self.name,
args=(result,),
daemon=True,
).start() ).start()