1

The Python ask-sdk for writing alexa skills provides two ways to write intent handlers. One by deriving from AbstractRequestHandler and implementing the two functions can_handle() and handle(). And another one using a function decorator (@SkillBuilder.request_handler()).

Using the second way with the decorator I am not able to call the handler functions directly (in unit tests). Trying to access the function the interpreter shows the error TypeError: 'NoneType' object is not callable.

The following is a minimal example of the intent handler as well as the testing code (which works similar to the suggestion at this github issue).

Intent handler

sb = SkillBuilder()
# Built-in Intent Handlers
@sb.request_handler(can_handle_func=is_request_type("LaunchRequest"))
def test_intent_handler(handler_input):
    return "Hello, test handler"

Test function

def test_intent():
    request = new_request('TestIntent', {})
    # THE FOLLOWING LINE THROWS THE ERROR
    response = test_intent_handler(request)

    expected_response = "Hello, test handler"
    assert response == expected_response

According to the answers to this question, the decorator function has to return a function, but this seems to be the case for request_handler() already as you can see on github

The decorator function does return a wrapper function, so test_intent_handler should be a function type. What am I missing?


EDIT

The answer of Adam Smith is a good workaround for this problem.

The reason that this happens is that the function SkillBuilder.request_handler returns a wrapper function which does not return anything. This wrapper function is used as the decorator for the handler function. Since the result of the decorator is assigned to test_intent_handler and the decorator (wrapper) does not return anything, the result is NoneType. So after decorating the handler with @sb.request_handler the original function is not accessible anymore.

To solve this the wrapper function just needs to return the passed-in handler function. Following Adam Smith's suggestion I created a pull request to change that, so that the "Alexa Skills Kit SDK for Python" can become more testable.

exilit
  • 1,156
  • 11
  • 23
  • Please show the full traceback. Note, `group_speakers_intent_handler` isn't the same as `test_intent_handler`. – Daniel Roseman Aug 04 '19 at 20:26
  • If you have time, this is the closest thing I've found to my current SO [post](https://stackoverflow.com/questions/73758162/access-the-alexa-shopping-and-to-do-lists-with-python3-request-module). Could you please have a look and offer any observations or suggestions? – kyrlon Oct 09 '22 at 16:42

1 Answers1

2

The purpose of a decorator is to modify a function seemingly in-place. Decorators don't (without some custom logic) keep references to their underlying function laying around exposed to callers. But that's fine, because what you're putting under test isn't the request handler -- it's the callback itself.

It's not unlikely that the ask-SDK has some framework for writing handler unit tests, but if it doesn't, just save off a reference of the callback for yourself.

# handler code

sb = SkillBuilder()

def _test_intent_handler(handler_input):
    return "Hello, test handler"

# Built-in Intent Handlers
@sb.request_handler(can_handle_func=is_request_type("LaunchRequest"))

test_intent_handler = sb.request_handler(
    can_handle_func=is_request_type("LaunchRequest"))(_test_intent_handler)
# test suite

def test_intent():
    request = new_request('TestIntent', {})
    response = _test_intent_handler(request)

    expected_response = "Hello, test handler"
    assert response == expected_response

If this bothers you (and I wouldn't blame you -- it's pretty ugly) you can write your own decorator that keeps that custom logic I mentioned above.

import functools

def meta_decorator(dec, *args, **kwargs):
    @functools.wraps(dec)
    def wrapper(f):
        @functools.wraps(f)
        def wrapped(*args, **kwargs):
            return f(*args, **kwargs)
        wrapped._original_function = f
        return dec(*args, **kwargs)(wrapped)
    return wrapper

sb = SkillBuilder()

@meta_decorator(sb.request_handler, can_handle_func=is_request_type("LaunchRequest"))
def test_intent_handler(handler_input):
    return "Hello, test handler"
# Test suite 2

def test_intent():
    request = new_request('Test Intent', {})
    response = test_intent_handler._original_function(request)
    expected_response = "Hello, test handler"
    assert response == expected_response
Adam Smith
  • 52,157
  • 12
  • 73
  • 112
  • I did not test it yet, but this seems to answer my question. There is one thing I still do not understand. As far as I know the logic behind the decoration should be equivalent to `test_intent_handler = sb.request_handler(can_handle_func=is_request_type("LaunchRequest"))(test_intent_handler)`. If this is true, I would expect `test_intent_handler` to be bound tho the returned wrapper function and therefore be of FunctionType (and callable). – exilit Aug 06 '19 at 10:07
  • 1
    I think I can answer my previous comment myself. `sb.request_handler` is not a decorator itself but generates a decorator. The decorator itself (which is called during decoration) does not return anything, which is bound to `test_intent_handler` and is in fact `NoneType`. I think this could be easily solved by the sdk, if the wrapper would return the passed-in function. – exilit Aug 06 '19 at 10:17
  • @exilit that appears to be correct. Since the SDK is open source, you should consider making a pull request! https://github.com/alexa/alexa-skills-kit-sdk-for-python/blob/master/ask-sdk-runtime/ask_sdk_runtime/skill_builder.py#L97 – Adam Smith Aug 06 '19 at 17:34
  • I definetly will consider doing that. – exilit Aug 08 '19 at 08:13