diff --git a/azure_functions_worker/bindings/datumdef.py b/azure_functions_worker/bindings/datumdef.py index d367b2fc2..2df29da47 100644 --- a/azure_functions_worker/bindings/datumdef.py +++ b/azure_functions_worker/bindings/datumdef.py @@ -199,6 +199,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 de3d69db6..c6148344d 100644 --- a/azure_functions_worker/bindings/meta.py +++ b/azure_functions_worker/bindings/meta.py @@ -270,9 +270,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..4dc852e37 --- /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" + } + ] +} \ No newline at end of file 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..4cced7c56 --- /dev/null +++ b/tests/unittests/generic_functions/foobar_nil_data/function.json @@ -0,0 +1,11 @@ +{ + "scriptFile": "main.py", + "bindings": [ + { + "type": "generic", + "name": "input", + "direction": "in" + } + ] + } + \ No newline at end of file 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..695d53110 100644 --- a/tests/unittests/test_mock_generic_functions.py +++ b/tests/unittests/test_mock_generic_functions.py @@ -144,6 +144,9 @@ 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 +198,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())