From 49835b0b69f0c7d733d7545d2b2f57794d7bca63 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Tue, 18 Oct 2022 10:12:59 -0500 Subject: [PATCH 1/5] convert asgi to use async handle --- azure/functions/decorators/function_app.py | 52 ++++++++----- tests/decorators/test_function_app.py | 90 +++++++++++----------- 2 files changed, 81 insertions(+), 61 deletions(-) diff --git a/azure/functions/decorators/function_app.py b/azure/functions/decorators/function_app.py index bf9e1128..27ab5bf1 100644 --- a/azure/functions/decorators/function_app.py +++ b/azure/functions/decorators/function_app.py @@ -1685,15 +1685,25 @@ class Blueprint(TriggerApi, BindingApi): class ExternalHttpFunctionApp(FunctionRegister, TriggerApi, ABC): """Interface to extend for building third party http function apps.""" + pass + + +class AsgiFunctionApp(ExternalHttpFunctionApp): + def __init__(self, app, + http_auth_level: Union[AuthLevel, str] = AuthLevel.FUNCTION): + """Constructor of :class:`AsgiFunctionApp` object. + + :param app: asgi app object. + """ + super().__init__(auth_level=http_auth_level) + self._add_http_app(AsgiMiddleware(app), 'asgi') def _add_http_app(self, - http_middleware: Union[ - AsgiMiddleware, WsgiMiddleware], + http_middleware: AsgiMiddleware, http_type: str) -> None: - """Add a Wsgi or Asgi app integrated http function. + """Add an Asgi app integrated http function. - :param http_middleware: :class:`AsgiMiddleware` or - :class:`WsgiMiddleware` instance. + :param http_middleware: :class:`AsgiMiddleware` instance. :return: None """ @@ -1702,19 +1712,8 @@ def _add_http_app(self, @self.route(methods=(method for method in HttpMethod), auth_level=self.auth_level, route="/{*route}") - def http_app_func(req: HttpRequest, context: Context): - return http_middleware.handle(req, context) - - -class AsgiFunctionApp(ExternalHttpFunctionApp): - def __init__(self, app, - http_auth_level: Union[AuthLevel, str] = AuthLevel.FUNCTION): - """Constructor of :class:`AsgiFunctionApp` object. - - :param app: asgi app object. - """ - super().__init__(auth_level=http_auth_level) - self._add_http_app(AsgiMiddleware(app), 'asgi') + async def http_app_func(req: HttpRequest, context: Context): + return await http_middleware.handle_async(req, context) class WsgiFunctionApp(ExternalHttpFunctionApp): @@ -1726,3 +1725,20 @@ def __init__(self, app, """ super().__init__(auth_level=http_auth_level) self._add_http_app(WsgiMiddleware(app), 'wsgi') + + def _add_http_app(self, + http_middleware: WsgiMiddleware, + http_type: str) -> None: + """Add a Wsgi app integrated http function. + + :param http_middleware: :class:`WsgiMiddleware` instance. + + :return: None + """ + + @self.http_type(http_type=http_type) + @self.route(methods=(method for method in HttpMethod), + auth_level=self.auth_level, + route="/{*route}") + def http_app_func(req: HttpRequest, context: Context): + return http_middleware.handle(req, context) diff --git a/tests/decorators/test_function_app.py b/tests/decorators/test_function_app.py index 7297db49..f0da90ca 100644 --- a/tests/decorators/test_function_app.py +++ b/tests/decorators/test_function_app.py @@ -1,5 +1,6 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. +import inspect import json import unittest from unittest import mock @@ -315,50 +316,11 @@ def test_add_wsgi(self, add_http_app_mock): WsgiMiddleware) self.assertEqual(add_http_app_mock.call_args[0][1], 'wsgi') - def test_add_http_app(self): - app = AsgiFunctionApp(app=object()) - funcs = app.get_functions() - self.assertEqual(len(funcs), 1) - func = funcs[0] - - self.assertEqual(func.get_function_name(), "http_app_func") + def test_add_asgi_app(self): + self._test_http_external_app(AsgiFunctionApp(app=object()), True) - raw_bindings = func.get_raw_bindings() - raw_trigger = raw_bindings[0] - raw_output_binding = raw_bindings[0] - - self.assertEqual(json.loads(raw_trigger), - json.loads( - '{"direction": "IN", "type": "httpTrigger", ' - '"authLevel": "FUNCTION", "route": "/{*route}", ' - '"methods": ["GET", "POST", "DELETE", "HEAD", ' - '"PATCH", "PUT", "OPTIONS"], "name": "req"}')) - self.assertEqual(json.loads(raw_output_binding), json.loads( - '{"direction": "IN", "type": "httpTrigger", "authLevel": ' - '"FUNCTION", "methods": ["GET", "POST", "DELETE", "HEAD", ' - '"PATCH", "PUT", "OPTIONS"], "name": "req", "route": "/{' - '*route}"}')) - - self.assertEqual(func.get_bindings_dict(), { - "bindings": [ - { - "authLevel": AuthLevel.FUNCTION, - "direction": BindingDirection.IN, - "methods": [HttpMethod.GET, HttpMethod.POST, - HttpMethod.DELETE, - HttpMethod.HEAD, - HttpMethod.PATCH, - HttpMethod.PUT, HttpMethod.OPTIONS], - "name": "req", - "route": "/{*route}", - "type": HTTP_TRIGGER - }, - { - "direction": BindingDirection.OUT, - "name": "$return", - "type": HTTP_OUTPUT - } - ]}) + def test_add_wsgi_app(self): + self._test_http_external_app(WsgiFunctionApp(app=object()), False) def test_register_function_app_error(self): with self.assertRaises(TypeError) as err: @@ -567,3 +529,45 @@ def test_wsgi_function_app_is_http_function(self): self.assertEqual(len(funcs), 1) self.assertTrue(funcs[0].is_http_function()) + + def _test_http_external_app(self, app, is_async): + funcs = app.get_functions() + self.assertEqual(len(funcs), 1) + func = funcs[0] + self.assertEqual(func.get_function_name(), "http_app_func") + raw_bindings = func.get_raw_bindings() + raw_trigger = raw_bindings[0] + raw_output_binding = raw_bindings[0] + self.assertEqual(inspect.iscoroutinefunction(func.get_user_function()), + is_async) + self.assertEqual(json.loads(raw_trigger), + json.loads( + '{"direction": "IN", "type": "httpTrigger", ' + '"authLevel": "FUNCTION", "route": "/{*route}", ' + '"methods": ["GET", "POST", "DELETE", "HEAD", ' + '"PATCH", "PUT", "OPTIONS"], "name": "req"}')) + self.assertEqual(json.loads(raw_output_binding), json.loads( + '{"direction": "IN", "type": "httpTrigger", "authLevel": ' + '"FUNCTION", "methods": ["GET", "POST", "DELETE", "HEAD", ' + '"PATCH", "PUT", "OPTIONS"], "name": "req", "route": "/{' + '*route}"}')) + self.assertEqual(func.get_bindings_dict(), { + "bindings": [ + { + "authLevel": AuthLevel.FUNCTION, + "direction": BindingDirection.IN, + "methods": [HttpMethod.GET, HttpMethod.POST, + HttpMethod.DELETE, + HttpMethod.HEAD, + HttpMethod.PATCH, + HttpMethod.PUT, HttpMethod.OPTIONS], + "name": "req", + "route": "/{*route}", + "type": HTTP_TRIGGER + }, + { + "direction": BindingDirection.OUT, + "name": "$return", + "type": HTTP_OUTPUT + } + ]}) From 939608de8a1cdf0082ac58a5a38b116c6b610817 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Tue, 18 Oct 2022 10:42:22 -0500 Subject: [PATCH 2/5] refactor --- azure/functions/decorators/function_app.py | 26 +++++++++++----------- tests/decorators/test_function_app.py | 3 --- 2 files changed, 13 insertions(+), 16 deletions(-) diff --git a/azure/functions/decorators/function_app.py b/azure/functions/decorators/function_app.py index 27ab5bf1..87127b45 100644 --- a/azure/functions/decorators/function_app.py +++ b/azure/functions/decorators/function_app.py @@ -1694,26 +1694,27 @@ def __init__(self, app, """Constructor of :class:`AsgiFunctionApp` object. :param app: asgi app object. + :param http_auth_level: Determines what keys, if any, need to be + present + on the request in order to invoke the function. """ super().__init__(auth_level=http_auth_level) - self._add_http_app(AsgiMiddleware(app), 'asgi') + self._add_http_app(AsgiMiddleware(app)) - def _add_http_app(self, - http_middleware: AsgiMiddleware, - http_type: str) -> None: + def _add_http_app(self, asgi_middleware: AsgiMiddleware) -> None: """Add an Asgi app integrated http function. - :param http_middleware: :class:`AsgiMiddleware` instance. + :param asgi_middleware: :class:`AsgiMiddleware` instance. :return: None """ - @self.http_type(http_type=http_type) + @self.http_type(http_type='asgi') @self.route(methods=(method for method in HttpMethod), auth_level=self.auth_level, route="/{*route}") async def http_app_func(req: HttpRequest, context: Context): - return await http_middleware.handle_async(req, context) + return await asgi_middleware.handle_async(req, context) class WsgiFunctionApp(ExternalHttpFunctionApp): @@ -1724,21 +1725,20 @@ def __init__(self, app, :param app: wsgi app object. """ super().__init__(auth_level=http_auth_level) - self._add_http_app(WsgiMiddleware(app), 'wsgi') + self._add_http_app(WsgiMiddleware(app)) def _add_http_app(self, - http_middleware: WsgiMiddleware, - http_type: str) -> None: + wsgi_middleware: WsgiMiddleware) -> None: """Add a Wsgi app integrated http function. - :param http_middleware: :class:`WsgiMiddleware` instance. + :param wsgi_middleware: :class:`WsgiMiddleware` instance. :return: None """ - @self.http_type(http_type=http_type) + @self.http_type(http_type='wsgi') @self.route(methods=(method for method in HttpMethod), auth_level=self.auth_level, route="/{*route}") def http_app_func(req: HttpRequest, context: Context): - return http_middleware.handle(req, context) + return wsgi_middleware.handle(req, context) diff --git a/tests/decorators/test_function_app.py b/tests/decorators/test_function_app.py index f0da90ca..08403287 100644 --- a/tests/decorators/test_function_app.py +++ b/tests/decorators/test_function_app.py @@ -303,8 +303,6 @@ def test_add_asgi(self, add_http_app_mock): self.assertIsInstance(add_http_app_mock.call_args[0][0], AsgiMiddleware) - self.assertEqual(add_http_app_mock.call_args[0][1], 'asgi') - @mock.patch('azure.functions.decorators.function_app.WsgiFunctionApp' '._add_http_app') def test_add_wsgi(self, add_http_app_mock): @@ -314,7 +312,6 @@ def test_add_wsgi(self, add_http_app_mock): add_http_app_mock.assert_called_once() self.assertIsInstance(add_http_app_mock.call_args[0][0], WsgiMiddleware) - self.assertEqual(add_http_app_mock.call_args[0][1], 'wsgi') def test_add_asgi_app(self): self._test_http_external_app(AsgiFunctionApp(app=object()), True) From 7bb63595fc5bfd21bde86ef43c4234d7c7f600b9 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Wed, 7 Dec 2022 17:58:59 +0000 Subject: [PATCH 3/5] remove externalhttpapp class --- azure/functions/__init__.py | 4 +--- azure/functions/decorators/__init__.py | 4 +--- azure/functions/decorators/function_app.py | 9 ++------- 3 files changed, 4 insertions(+), 13 deletions(-) diff --git a/azure/functions/__init__.py b/azure/functions/__init__.py index edda0c67..ec508652 100644 --- a/azure/functions/__init__.py +++ b/azure/functions/__init__.py @@ -11,8 +11,7 @@ Cardinality, AccessRights, HttpMethod, AsgiFunctionApp, WsgiFunctionApp) from ._durable_functions import OrchestrationContext, EntityContext -from .decorators.function_app import (FunctionRegister, TriggerApi, BindingApi, - ExternalHttpFunctionApp) +from .decorators.function_app import (FunctionRegister, TriggerApi, BindingApi) from .extension import (ExtensionMeta, FunctionExtensionException, FuncExtensionBase, AppExtensionBase) from ._http_wsgi import WsgiMiddleware @@ -83,7 +82,6 @@ 'TriggerApi', 'BindingApi', 'Blueprint', - 'ExternalHttpFunctionApp', 'AsgiFunctionApp', 'WsgiFunctionApp', 'DataType', diff --git a/azure/functions/decorators/__init__.py b/azure/functions/decorators/__init__.py index 83a96447..234efd65 100644 --- a/azure/functions/decorators/__init__.py +++ b/azure/functions/decorators/__init__.py @@ -2,8 +2,7 @@ # Licensed under the MIT License. from .core import Cardinality, AccessRights from .function_app import FunctionApp, Function, DecoratorApi, DataType, \ - AuthLevel, Blueprint, AsgiFunctionApp, WsgiFunctionApp, \ - ExternalHttpFunctionApp, FunctionRegister, TriggerApi, BindingApi + AuthLevel, Blueprint, AsgiFunctionApp, WsgiFunctionApp, FunctionRegister, TriggerApi, BindingApi from .http import HttpMethod __all__ = [ @@ -14,7 +13,6 @@ 'TriggerApi', 'BindingApi', 'Blueprint', - 'ExternalHttpFunctionApp', 'AsgiFunctionApp', 'WsgiFunctionApp', 'DataType', diff --git a/azure/functions/decorators/function_app.py b/azure/functions/decorators/function_app.py index 87127b45..ab34eff7 100644 --- a/azure/functions/decorators/function_app.py +++ b/azure/functions/decorators/function_app.py @@ -1683,12 +1683,7 @@ class Blueprint(TriggerApi, BindingApi): pass -class ExternalHttpFunctionApp(FunctionRegister, TriggerApi, ABC): - """Interface to extend for building third party http function apps.""" - pass - - -class AsgiFunctionApp(ExternalHttpFunctionApp): +class AsgiFunctionApp(FunctionRegister, TriggerApi): def __init__(self, app, http_auth_level: Union[AuthLevel, str] = AuthLevel.FUNCTION): """Constructor of :class:`AsgiFunctionApp` object. @@ -1717,7 +1712,7 @@ async def http_app_func(req: HttpRequest, context: Context): return await asgi_middleware.handle_async(req, context) -class WsgiFunctionApp(ExternalHttpFunctionApp): +class WsgiFunctionApp(FunctionRegister, TriggerApi): def __init__(self, app, http_auth_level: Union[AuthLevel, str] = AuthLevel.FUNCTION): """Constructor of :class:`WsgiFunctionApp` object. From aad4a9f993ae5f790a74a854ae93837cea6c6808 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Wed, 7 Dec 2022 19:52:32 +0000 Subject: [PATCH 4/5] fix mypy --- azure/functions/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/azure/functions/__init__.py b/azure/functions/__init__.py index ec508652..3631b757 100644 --- a/azure/functions/__init__.py +++ b/azure/functions/__init__.py @@ -11,7 +11,8 @@ Cardinality, AccessRights, HttpMethod, AsgiFunctionApp, WsgiFunctionApp) from ._durable_functions import OrchestrationContext, EntityContext -from .decorators.function_app import (FunctionRegister, TriggerApi, BindingApi) +from .decorators.function_app import (FunctionRegister, TriggerApi, + BindingApi) from .extension import (ExtensionMeta, FunctionExtensionException, FuncExtensionBase, AppExtensionBase) from ._http_wsgi import WsgiMiddleware From a0a4f0536cf01696a7e29220e06957fa2f6f265f Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Wed, 7 Dec 2022 19:54:06 +0000 Subject: [PATCH 5/5] fix mypy --- azure/functions/decorators/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/azure/functions/decorators/__init__.py b/azure/functions/decorators/__init__.py index 234efd65..8bbac92d 100644 --- a/azure/functions/decorators/__init__.py +++ b/azure/functions/decorators/__init__.py @@ -2,7 +2,8 @@ # Licensed under the MIT License. from .core import Cardinality, AccessRights from .function_app import FunctionApp, Function, DecoratorApi, DataType, \ - AuthLevel, Blueprint, AsgiFunctionApp, WsgiFunctionApp, FunctionRegister, TriggerApi, BindingApi + AuthLevel, Blueprint, AsgiFunctionApp, WsgiFunctionApp, \ + FunctionRegister, TriggerApi, BindingApi from .http import HttpMethod __all__ = [