diff --git a/workers/azure_functions_worker/functions.py b/workers/azure_functions_worker/functions.py index 5feac5b4c..a0773440e 100644 --- a/workers/azure_functions_worker/functions.py +++ b/workers/azure_functions_worker/functions.py @@ -278,6 +278,7 @@ def get_function_return_type(annotations: dict, has_explicit_return: bool, return_anno = annotations.get('return') if typing_inspect.is_generic_type( return_anno) and typing_inspect.get_origin( + return_anno) is not None and typing_inspect.get_origin( return_anno).__name__ == 'Out': raise FunctionLoadError( func_name, diff --git a/workers/tests/endtoend/http_functions/http_functions_stein/function_app.py b/workers/tests/endtoend/http_functions/http_functions_stein/function_app.py index 512cf83c2..434f3c44c 100644 --- a/workers/tests/endtoend/http_functions/http_functions_stein/function_app.py +++ b/workers/tests/endtoend/http_functions/http_functions_stein/function_app.py @@ -1,44 +1,87 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -import logging -import time -from datetime import datetime - -import azure.functions as func - -app = func.FunctionApp(http_auth_level=func.AuthLevel.ANONYMOUS) - - -@app.route(route="default_template") -def default_template(req: func.HttpRequest) -> func.HttpResponse: - logging.info('Python HTTP trigger function processed a request.') - - name = req.params.get('name') - if not name: - try: - req_body = req.get_json() - except ValueError: - pass - else: - name = req_body.get('name') - - if name: - return func.HttpResponse( - f"Hello, {name}. This HTTP triggered function " - f"executed successfully.") - else: - return func.HttpResponse( - "This HTTP triggered function executed successfully. " - "Pass a name in the query string or in the request body for a" - " personalized response.", - status_code=200 - ) - - -@app.route(route="http_func") -def http_func(req: func.HttpRequest) -> func.HttpResponse: - time.sleep(1) - - current_time = datetime.now().strftime("%H:%M:%S") - return func.HttpResponse(f"{current_time}") +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import json +import logging +import time + +from datetime import datetime +from typing import Generic, Mapping, Optional, TypeVar, Union + +import azure.functions as func + +app = func.FunctionApp(http_auth_level=func.AuthLevel.ANONYMOUS) + +JsonType = Union[list, tuple, dict, str, int, float, bool] +T = TypeVar("T", bound=JsonType) + + +class JsonResponse(Generic[T], func.HttpResponse): + def __init__( + self, + body: T, + status_code: int = 200, + headers: Optional[Mapping[str, str]] = None, + ): + super().__init__(json.dumps(body), + status_code=status_code, + headers=headers, + charset="utf-8") + + +@app.route(route="default_template") +def default_template(req: func.HttpRequest) -> func.HttpResponse: + logging.info('Python HTTP trigger function processed a request.') + + name = req.params.get('name') + if not name: + try: + req_body = req.get_json() + except ValueError: + pass + else: + name = req_body.get('name') + + if name: + return func.HttpResponse( + f"Hello, {name}. This HTTP triggered function " + f"executed successfully.") + else: + return func.HttpResponse( + "This HTTP triggered function executed successfully. " + "Pass a name in the query string or in the request body for a" + " personalized response.", + status_code=200 + ) + + +@app.route(route="http_func") +def http_func(req: func.HttpRequest) -> func.HttpResponse: + time.sleep(1) + + current_time = datetime.now().strftime("%H:%M:%S") + return func.HttpResponse(f"{current_time}") + + +@app.route(route="custom_response") +def custom_response(req: func.HttpRequest) -> JsonResponse: + name = req.params.get('name') + if not name: + try: + req_body = req.get_json() + except ValueError: + pass + else: + name = req_body.get('name') + if name: + return JsonResponse( + { + "name": name + }, + ) + else: + return JsonResponse( + { + "status": "healthy" + }, + ) diff --git a/workers/tests/endtoend/http_functions/http_functions_stein/generic/function_app.py b/workers/tests/endtoend/http_functions/http_functions_stein/generic/function_app.py index a1bf66cdc..84591ec33 100644 --- a/workers/tests/endtoend/http_functions/http_functions_stein/generic/function_app.py +++ b/workers/tests/endtoend/http_functions/http_functions_stein/generic/function_app.py @@ -1,38 +1,85 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -import logging - -import azure.functions as func - -app = func.FunctionApp(http_auth_level=func.AuthLevel.ANONYMOUS) - - -@app.function_name(name="default_template") -@app.generic_trigger(arg_name="req", - type="httpTrigger", - route="default_template") -@app.generic_output_binding(arg_name="$return", type="http") -def default_template(req: func.HttpRequest) -> func.HttpResponse: - logging.info('Python HTTP trigger function processed a request.') - - name = req.params.get('name') - if not name: - try: - req_body = req.get_json() - except ValueError: - pass - else: - name = req_body.get('name') - - if name: - return func.HttpResponse( - f"Hello, {name}. This HTTP triggered function " - f"executed successfully.") - else: - return func.HttpResponse( - "This HTTP triggered function executed successfully. " - "Pass a name in the query string or in the request body for a" - " personalized response.", - status_code=200 - ) +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import json +import logging + +from typing import Generic, Mapping, Optional, TypeVar, Union + +import azure.functions as func + +app = func.FunctionApp(http_auth_level=func.AuthLevel.ANONYMOUS) + + +JsonType = Union[list, tuple, dict, str, int, float, bool] +T = TypeVar("T", bound=JsonType) + + +class JsonResponse(Generic[T], func.HttpResponse): + def __init__( + self, + body: T, + status_code: int = 200, + headers: Optional[Mapping[str, str]] = None, + ): + super().__init__(json.dumps(body), + status_code=status_code, + headers=headers, + charset="utf-8") + + +@app.function_name(name="default_template") +@app.generic_trigger(arg_name="req", + type="httpTrigger", + route="default_template") +@app.generic_output_binding(arg_name="$return", type="http") +def default_template(req: func.HttpRequest) -> func.HttpResponse: + logging.info('Python HTTP trigger function processed a request.') + + name = req.params.get('name') + if not name: + try: + req_body = req.get_json() + except ValueError: + pass + else: + name = req_body.get('name') + + if name: + return func.HttpResponse( + f"Hello, {name}. This HTTP triggered function " + f"executed successfully.") + else: + return func.HttpResponse( + "This HTTP triggered function executed successfully. " + "Pass a name in the query string or in the request body for a" + " personalized response.", + status_code=200 + ) + + +@app.generic_trigger(arg_name="req", + type="httpTrigger", + route="custom_response") +@app.generic_output_binding(arg_name="$return", type="http") +def custom_response(req: func.HttpRequest) -> JsonResponse: + name = req.params.get('name') + if not name: + try: + req_body = req.get_json() + except ValueError: + pass + else: + name = req_body.get('name') + if name: + return JsonResponse( + { + "name": name + }, + ) + else: + return JsonResponse( + { + "status": "healthy" + }, + ) diff --git a/workers/tests/endtoend/test_http_functions.py b/workers/tests/endtoend/test_http_functions.py index 3128dfd38..b13827d18 100644 --- a/workers/tests/endtoend/test_http_functions.py +++ b/workers/tests/endtoend/test_http_functions.py @@ -118,8 +118,33 @@ def get_script_dir(cls): return testutils.E2E_TESTS_FOLDER / 'http_functions' / \ 'http_functions_stein' + @testutils.retryable_test(3, 5) + def test_return_custom_class(self): + """Test if returning a custom class returns OK + """ + r = self.webhost.request('GET', 'custom_response', + timeout=REQUEST_TIMEOUT_SEC) + self.assertEqual( + r.content, + b'{"status": "healthy"}' + ) + self.assertTrue(r.ok) + + @testutils.retryable_test(3, 5) + def test_return_custom_class_with_query_param(self): + """Test if query is accepted + """ + r = self.webhost.request('GET', 'custom_response', + params={'name': 'query'}, + timeout=REQUEST_TIMEOUT_SEC) + self.assertTrue(r.ok) + self.assertEqual( + r.content, + b'{"name": "query"}' + ) + -class TestHttpFunctionsSteinGeneric(TestHttpFunctions): +class TestHttpFunctionsSteinGeneric(TestHttpFunctionsStein): @classmethod def get_script_dir(cls): diff --git a/workers/tests/unittests/test_typing_inspect.py b/workers/tests/unittests/test_typing_inspect.py index 4f01e4c73..a7d7a4d2d 100644 --- a/workers/tests/unittests/test_typing_inspect.py +++ b/workers/tests/unittests/test_typing_inspect.py @@ -98,11 +98,14 @@ class GetUtilityTestCase(TestCase): def test_origin(self): T = TypeVar('T') + class MyClass(Generic[T]): pass + self.assertEqual(get_origin(int), None) self.assertEqual(get_origin(ClassVar[int]), None) self.assertEqual(get_origin(Generic), Generic) self.assertEqual(get_origin(Generic[T]), Generic) self.assertEqual(get_origin(List[Tuple[T, T]][int]), list) + self.assertEqual(get_origin(MyClass), None) def test_parameters(self): T = TypeVar('T')