Skip to content

Commit

Permalink
Documents decorators
Browse files Browse the repository at this point in the history
Implements unit tests for common decorators and stubs tests for less used ones
  • Loading branch information
NeonDaniel committed Jul 7, 2023
1 parent 0a6c483 commit 98f655a
Show file tree
Hide file tree
Showing 8 changed files with 338 additions and 83 deletions.
91 changes: 53 additions & 38 deletions ovos_workshop/decorators/__init__.py
Original file line number Diff line number Diff line change
@@ -1,24 +1,15 @@
from ovos_utils.log import log_deprecation
from ovos_workshop.decorators.killable import killable_intent, killable_event
from ovos_workshop.decorators.layers import enables_layer, \
disables_layer, layer_intent, removes_layer, resets_layers, replaces_layer
from ovos_workshop.decorators.converse import converse_handler
from ovos_workshop.decorators.fallback_handler import fallback_handler
from ovos_utils import classproperty
from ovos_workshop.decorators.ocp import ocp_play, ocp_pause, ocp_resume, \
ocp_search, ocp_previous, ocp_featured_media
from functools import wraps
try:
from ovos_workshop.decorators.ocp import ocp_next, ocp_play, ocp_pause, ocp_resume, ocp_search, ocp_previous, ocp_featured_media
except ImportError:
pass # these imports are only available if extra requirements are installed

"""
Decorators for use with MycroftSkill methods

Helper decorators for handling context from skills.
"""


def adds_context(context, words=''):
"""Decorator adding context to the Adapt context manager.
def adds_context(context: str, words: str = ''):
"""
Decorator to add context to the Adapt context manager.
Args:
context (str): context Keyword to insert
Expand All @@ -31,14 +22,13 @@ def func_wrapper(*args, **kwargs):
ret = func(*args, **kwargs)
args[0].set_context(context, words)
return ret

return func_wrapper

return context_add_decorator


def removes_context(context):
"""Decorator removing context from the Adapt context manager.
def removes_context(context: str):
"""
Decorator to remove context from the Adapt context manager.
Args:
context (str): Context keyword to remove
Expand All @@ -50,14 +40,15 @@ def func_wrapper(*args, **kwargs):
ret = func(*args, **kwargs)
args[0].remove_context(context)
return ret

return func_wrapper

return context_removes_decorator


def intent_handler(intent_parser):
"""Decorator for adding a method as an intent handler."""
def intent_handler(intent_parser: object):
"""
Decorator for adding a method as an intent handler.
@param intent_parser: string intent name or adapt.IntentBuilder object
"""

def real_decorator(func):
# Store the intent_parser inside the function
Expand All @@ -70,27 +61,26 @@ def real_decorator(func):
return real_decorator


def intent_file_handler(intent_file):
"""Decorator for adding a method as an intent file handler.
This decorator is deprecated, use intent_handler for the same effect.
def intent_file_handler(intent_file: str):
"""
Deprecated decorator for adding a method as an intent file handler.
"""

def real_decorator(func):
# Store the intent_file inside the function
# This will be used later to call register_intent_file
if not hasattr(func, 'intent_files'):
func.intent_files = []
func.intent_files.append(intent_file)
return func

log_deprecation(f"Use `@intent_handler({intent_file})`", "0.1.0")
return real_decorator


def resting_screen_handler(name):
"""Decorator for adding a method as an resting screen handler.
If selected will be shown on screen when device enters idle mode.
def resting_screen_handler(name: str):
"""
Decorator for adding a method as a resting screen handler to optionally
be shown on screen when device enters idle mode.
@param name: Name of the restring screen to register
"""

def real_decorator(func):
Expand All @@ -103,13 +93,38 @@ def real_decorator(func):
return real_decorator


def skill_api_method(func):
"""Decorator for adding a method to the skill's public api.
Methods with this decorator will be registered on the message bus
and an api object can be created for interaction with the skill.
def skill_api_method(func: callable):
"""
Decorator for adding a method to the skill's public api. Methods with this
decorator will be registered on the messagebus and an api object can be
created for interaction with the skill.
@param func: API method to expose
"""
# tag the method by adding an api_method member to it
if not hasattr(func, 'api_method') and hasattr(func, '__name__'):
func.api_method = True
return func


def converse_handler(func):
"""
Decorator for aliasing a method as the converse method
"""
if not hasattr(func, 'converse'):
func.converse = True
return func


def fallback_handler(priority: int = 50):
"""
Decorator for adding a fallback intent handler.
@param priority: Fallback priority (0-100) with lower values having higher
priority
"""
def real_decorator(func):
if not hasattr(func, 'fallback_priority'):
func.fallback_priority = priority
return func

return real_decorator
18 changes: 9 additions & 9 deletions ovos_workshop/decorators/converse.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
from ovos_utils.log import log_deprecation


def converse_handler():
"""Decorator for aliasing a method as the converse method"""

def real_decorator(func):
if not hasattr(func, 'converse'):
func.converse = True
return func

return real_decorator
def converse_handler(func):
"""
Decorator for aliasing a method as the converse method
"""
log_deprecation("Import from `ovos_workshop.decorators`", "0.1.0")
if not hasattr(func, 'converse'):
func.converse = True
return func
4 changes: 4 additions & 0 deletions ovos_workshop/decorators/fallback_handler.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
from ovos_utils.log import log_deprecation


def fallback_handler(priority=50):
log_deprecation("Import from `ovos_workshop.decorators`", "0.1.0")

def real_decorator(func):
if not hasattr(func, 'fallback_priority'):
func.fallback_priority = priority
Expand Down
37 changes: 29 additions & 8 deletions ovos_workshop/decorators/killable.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from typing import Callable, Optional, Type

import time
from ovos_utils import create_killable_daemon
from ovos_utils.messagebus import Message
Expand All @@ -18,16 +20,36 @@ class AbortQuestion(AbortEvent):
""" gracefully abort get_response queries """


def killable_intent(msg="mycroft.skills.abort_execution",
callback=None, react_to_stop=True, call_stop=True,
stop_tts=True):
def killable_intent(msg: str = "mycroft.skills.abort_execution",
callback: Optional[callable] = None,
react_to_stop: bool = True,
call_stop: bool = True, stop_tts: bool = True) -> callable:
"""
Decorator to mark an intent that can be terminated during execution.
@param msg: Message name to terminate on
@param callback: Optional function or method to call on termination
@param react_to_stop: If true, also terminate on `stop` Messages
@param call_stop: If true, also call `Class.stop` method
@param stop_tts: If true, emit message to stop TTS audio playback
"""
return killable_event(msg, AbortIntent, callback, react_to_stop,
call_stop, stop_tts)


def killable_event(msg="mycroft.skills.abort_execution", exc=AbortEvent,
callback=None, react_to_stop=False, call_stop=False,
stop_tts=False):
def killable_event(msg: str = "mycroft.skills.abort_execution",
exc: Type[Exception] = AbortEvent,
callback: Optional[callable] = None,
react_to_stop: bool = False, call_stop: bool = False,
stop_tts: bool = False):
"""
Decorator to mark a method that can be terminated during execution.
@param msg: Message name to terminate on
@param exc: Exception to raise in killed thread
@param callback: Optional function or method to call on termination
@param react_to_stop: If true, also terminate on `stop` Messages
@param call_stop: If true, also call `Class.stop` method
@param stop_tts: If true, emit message to stop TTS audio playback
"""
# Begin wrapper
def create_killable(func):

Expand All @@ -54,7 +76,7 @@ def abort(_):
try:
while t.is_alive():
t.raise_exc(exc)
time.sleep(0.1)
t.join(1)
except threading.ThreadError:
pass # already killed
except AssertionError:
Expand All @@ -79,4 +101,3 @@ def abort(_):
return call_function

return create_killable

34 changes: 27 additions & 7 deletions ovos_workshop/decorators/layers.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,16 @@
import inspect
from functools import wraps
from typing import Optional, List

from ovos_utils.log import LOG


def dig_for_skill(max_records: int = 10):
def dig_for_skill(max_records: int = 10) -> Optional[object]:
"""
Dig through the call stack to locate a Skill object
@param max_records: maximum number of records in the stack to check
@return: Skill or AbstractApplication instance if found
"""
from ovos_workshop.app import OVOSAbstractApplication
from ovos_workshop.skills import MycroftSkill
stack = inspect.stack()[1:] # First frame will be this function call
Expand All @@ -23,13 +30,17 @@ def dig_for_skill(max_records: int = 10):
return None


def enables_layer(layer_name):
def enables_layer(layer_name: str):
"""
Decorator to enable an intent layer when a method is called
@param layer_name: name of intent layer to enable
"""
def layer_handler(func):
@wraps(func)
def call_function(*args, **kwargs):
skill = dig_for_skill()
skill.intent_layers = skill.intent_layers or \
IntentLayers().bind(skill)
IntentLayers().bind(skill)
func(*args, **kwargs)
skill.intent_layers.activate_layer(layer_name)

Expand All @@ -38,13 +49,17 @@ def call_function(*args, **kwargs):
return layer_handler


def disables_layer(layer_name):
def disables_layer(layer_name: str):
"""
Decorator to disable an intent layer when a method is called
@param layer_name: name of intent layer to disable
"""
def layer_handler(func):
@wraps(func)
def call_function(*args, **kwargs):
skill = dig_for_skill()
skill.intent_layers = skill.intent_layers or \
IntentLayers().bind(skill)
IntentLayers().bind(skill)
func(*args, **kwargs)
skill.intent_layers.deactivate_layer(layer_name)

Expand All @@ -53,13 +68,18 @@ def call_function(*args, **kwargs):
return layer_handler


def replaces_layer(layer_name, intent_list):
def replaces_layer(layer_name: str, intent_list: Optional[List[str]]):
"""
Replaces intents at the specified layer
@param layer_name: name of intent layer to replace
@param intent_list: list of new intents for the specified layer
"""
def layer_handler(func):
@wraps(func)
def call_function(*args, **kwargs):
skill = dig_for_skill()
skill.intent_layers = skill.intent_layers or \
IntentLayers().bind(skill)
IntentLayers().bind(skill)
func(*args, **kwargs)
skill.intent_layers.replace_layer(layer_name, intent_list)

Expand Down
Loading

0 comments on commit 98f655a

Please sign in to comment.