From 75f813cb02d9cdefac636d16e3492c17872f80a4 Mon Sep 17 00:00:00 2001 From: hallvictoria Date: Thu, 18 Apr 2024 11:33:14 -0500 Subject: [PATCH] allow return none for generic bindings --- azure_functions_worker/bindings/datumdef.py | 2 ++ azure_functions_worker/bindings/generic.py | 9 ++++++- azure_functions_worker/bindings/meta.py | 3 +-- .../generic_functions_stein/function_app.py | 14 ++++++++++ .../return_none/function.json | 21 +++++++++++++++ .../generic_functions/return_none/main.py | 10 +++++++ tests/endtoend/test_generic_functions.py | 14 ++++++++++ .../foobar_nil_data/function.json | 10 +++++++ .../generic_functions/foobar_nil_data/main.py | 7 +++++ .../unittests/test_mock_generic_functions.py | 27 +++++++++++++++++++ 10 files changed, 114 insertions(+), 3 deletions(-) create mode 100644 tests/endtoend/generic_functions/return_none/function.json create mode 100644 tests/endtoend/generic_functions/return_none/main.py create mode 100644 tests/unittests/generic_functions/foobar_nil_data/function.json create mode 100644 tests/unittests/generic_functions/foobar_nil_data/main.py diff --git a/azure_functions_worker/bindings/datumdef.py b/azure_functions_worker/bindings/datumdef.py index b420fbf3b..506221375 100644 --- a/azure_functions_worker/bindings/datumdef.py +++ b/azure_functions_worker/bindings/datumdef.py @@ -197,6 +197,8 @@ def datum_as_proto(datum: Datum) -> protos.TypedData: enable_content_negotiation=False, body=datum_as_proto(datum.value['body']), )) + elif datum.type is None: + return None else: raise NotImplementedError( 'unexpected Datum type: {!r}'.format(datum.type) diff --git a/azure_functions_worker/bindings/generic.py b/azure_functions_worker/bindings/generic.py index bc886dee0..28db5fe78 100644 --- a/azure_functions_worker/bindings/generic.py +++ b/azure_functions_worker/bindings/generic.py @@ -28,12 +28,17 @@ def encode(cls, obj: Any, *, elif isinstance(obj, (bytes, bytearray)): return datumdef.Datum(type='bytes', value=bytes(obj)) - + elif obj is None: + return datumdef.Datum(type=None, value=obj) else: raise NotImplementedError @classmethod def decode(cls, data: datumdef.Datum, *, trigger_metadata) -> typing.Any: + # Enabling support for Dapr bindings + # https://github.com/Azure/azure-functions-python-worker/issues/1316 + if data is None: + return None data_type = data.type if data_type == 'string': @@ -42,6 +47,8 @@ def decode(cls, data: datumdef.Datum, *, trigger_metadata) -> typing.Any: result = data.value elif data_type == 'json': result = data.value + elif data_type is None: + result = None else: raise ValueError( f'unexpected type of data received for the "generic" binding ' diff --git a/azure_functions_worker/bindings/meta.py b/azure_functions_worker/bindings/meta.py index f7a810145..c00c2e97d 100644 --- a/azure_functions_worker/bindings/meta.py +++ b/azure_functions_worker/bindings/meta.py @@ -178,9 +178,8 @@ def to_outgoing_param_binding(binding: str, obj: typing.Any, *, rpc_shared_memory=shared_mem_value) else: # If not, send it as part of the response message over RPC + # rpc_val can be None here as we now support a None return type rpc_val = datumdef.datum_as_proto(datum) - if rpc_val is None: - raise TypeError('Cannot convert datum to rpc_val') return protos.ParameterBinding( name=out_name, data=rpc_val) diff --git a/tests/endtoend/generic_functions/generic_functions_stein/function_app.py b/tests/endtoend/generic_functions/generic_functions_stein/function_app.py index 47c74f862..2ac571849 100644 --- a/tests/endtoend/generic_functions/generic_functions_stein/function_app.py +++ b/tests/endtoend/generic_functions/generic_functions_stein/function_app.py @@ -1,6 +1,7 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. import azure.functions as func +import logging app = func.FunctionApp(http_auth_level=func.AuthLevel.ANONYMOUS) @@ -29,3 +30,16 @@ def return_processed_last(req: func.HttpRequest, testEntity): table_name="EventHubBatchTest") def return_not_processed_last(req: func.HttpRequest, testEntities): return func.HttpResponse(status_code=200) + + +@app.function_name(name="mytimer") +@app.schedule(schedule="*/1 * * * * *", arg_name="mytimer", + run_on_startup=False, + use_monitor=False) +@app.generic_input_binding( + arg_name="testEntity", + type="table", + connection="AzureWebJobsStorage", + table_name="EventHubBatchTest") +def mytimer(mytimer: func.TimerRequest, testEntity) -> None: + logging.info("This timer trigger function executed successfully") diff --git a/tests/endtoend/generic_functions/return_none/function.json b/tests/endtoend/generic_functions/return_none/function.json new file mode 100644 index 000000000..b84fb01f8 --- /dev/null +++ b/tests/endtoend/generic_functions/return_none/function.json @@ -0,0 +1,21 @@ +{ + "scriptFile": "main.py", + "bindings": [ + { + "name": "mytimer", + "type": "timerTrigger", + "direction": "in", + "schedule": "*/1 * * * * *", + "runOnStartup": false + }, + { + "direction": "in", + "type": "table", + "name": "testEntity", + "partitionKey": "test", + "rowKey": "WillBePopulatedWithGuid", + "tableName": "BindingTestTable", + "connection": "AzureWebJobsStorage" + } + ] + } diff --git a/tests/endtoend/generic_functions/return_none/main.py b/tests/endtoend/generic_functions/return_none/main.py new file mode 100644 index 000000000..8f52c716b --- /dev/null +++ b/tests/endtoend/generic_functions/return_none/main.py @@ -0,0 +1,10 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import logging + +import azure.functions as func + + +def main(mytimer: func.TimerRequest, testEntity) -> None: + logging.info("This timer trigger function executed successfully") diff --git a/tests/endtoend/test_generic_functions.py b/tests/endtoend/test_generic_functions.py index 8be60f669..a03571499 100644 --- a/tests/endtoend/test_generic_functions.py +++ b/tests/endtoend/test_generic_functions.py @@ -2,6 +2,9 @@ # Licensed under the MIT License. from unittest import skipIf +import time +import typing + from azure_functions_worker.utils.common import is_envvar_true from tests.utils import testutils from tests.utils.constants import DEDICATED_DOCKER_TEST, CONSUMPTION_DOCKER_TEST @@ -41,6 +44,17 @@ def test_return_not_processed_last(self): r = self.webhost.request('GET', 'return_not_processed_last') self.assertEqual(r.status_code, 200) + def test_return_none(self): + time.sleep(1) + # Checking webhost status. + r = self.webhost.request('GET', '', no_prefix=True, + timeout=5) + self.assertTrue(r.ok) + + def check_log_timer(self, host_out: typing.List[str]): + self.assertEqual(host_out.count("This timer trigger function executed " + "successfully"), 1) + @skipIf(is_envvar_true(DEDICATED_DOCKER_TEST) or is_envvar_true(CONSUMPTION_DOCKER_TEST), diff --git a/tests/unittests/generic_functions/foobar_nil_data/function.json b/tests/unittests/generic_functions/foobar_nil_data/function.json new file mode 100644 index 000000000..eb782682b --- /dev/null +++ b/tests/unittests/generic_functions/foobar_nil_data/function.json @@ -0,0 +1,10 @@ +{ + "scriptFile": "main.py", + "bindings": [ + { + "type": "generic", + "name": "input", + "direction": "in" + } + ] + } diff --git a/tests/unittests/generic_functions/foobar_nil_data/main.py b/tests/unittests/generic_functions/foobar_nil_data/main.py new file mode 100644 index 000000000..a41823ddc --- /dev/null +++ b/tests/unittests/generic_functions/foobar_nil_data/main.py @@ -0,0 +1,7 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +import logging + + +def main(input) -> None: + logging.info("Hello World") diff --git a/tests/unittests/test_mock_generic_functions.py b/tests/unittests/test_mock_generic_functions.py index 238837d89..2da007fde 100644 --- a/tests/unittests/test_mock_generic_functions.py +++ b/tests/unittests/test_mock_generic_functions.py @@ -144,6 +144,8 @@ async def test_mock_generic_should_support_implicit_output(self): # implicitly self.assertEqual(r.response.result.status, protos.StatusResult.Success) + self.assertEqual(r.response.return_value, + protos.TypedData(bytes=b'\x00\x01')) async def test_mock_generic_should_support_without_datatype(self): async with testutils.start_mockhost( @@ -195,3 +197,28 @@ async def test_mock_generic_implicit_output_exemption(self): # For the Durable Functions durableClient case self.assertEqual(r.response.result.status, protos.StatusResult.Failure) + + async def test_mock_generic_as_nil_data(self): + async with testutils.start_mockhost( + script_root=self.generic_funcs_dir) as host: + + await host.init_worker("4.17.1") + func_id, r = await host.load_function('foobar_nil_data') + + self.assertEqual(r.response.function_id, func_id) + self.assertEqual(r.response.result.status, + protos.StatusResult.Success) + + _, r = await host.invoke_function( + 'foobar_nil_data', [ + protos.ParameterBinding( + name='input', + data=protos.TypedData() + ) + ] + ) + self.assertEqual(r.response.result.status, + protos.StatusResult.Success) + self.assertEqual( + r.response.return_value, + protos.TypedData())