From daa85d2b9c44a82edf711a12494800772d50d7a6 Mon Sep 17 00:00:00 2001 From: hallvictoria Date: Mon, 11 Dec 2023 15:22:58 -0600 Subject: [PATCH 1/9] allow nill data for generic bindings --- azure_functions_worker/bindings/generic.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/azure_functions_worker/bindings/generic.py b/azure_functions_worker/bindings/generic.py index 9d0cca8af..a1b001519 100644 --- a/azure_functions_worker/bindings/generic.py +++ b/azure_functions_worker/bindings/generic.py @@ -34,6 +34,8 @@ def encode(cls, obj: Any, *, @classmethod def decode(cls, data: datumdef.Datum, *, trigger_metadata) -> typing.Any: + if data is None: + return None data_type = data.type if data_type == 'string': From 89c7835654a73ff81f251c530acca710cb6410d0 Mon Sep 17 00:00:00 2001 From: hallvictoria Date: Mon, 11 Dec 2023 16:41:00 -0600 Subject: [PATCH 2/9] tests --- .../foobar_nil_data/function.json | 11 ++++++++ .../generic_functions/foobar_nil_data/main.py | 6 +++++ .../unittests/test_mock_generic_functions.py | 25 +++++++++++++++++++ 3 files changed, 42 insertions(+) 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/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..c806f5f51 --- /dev/null +++ b/tests/unittests/generic_functions/foobar_nil_data/main.py @@ -0,0 +1,6 @@ +# 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 32004850f..57b358d88 100644 --- a/tests/unittests/test_mock_generic_functions.py +++ b/tests/unittests/test_mock_generic_functions.py @@ -170,3 +170,28 @@ async def test_mock_generic_should_support_without_datatype(self): # to be defined in function.json 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()) From b097d1449e65435e0d08733b722aca31c3d6730d Mon Sep 17 00:00:00 2001 From: hallvictoria Date: Mon, 11 Dec 2023 16:42:49 -0600 Subject: [PATCH 3/9] lint --- tests/unittests/generic_functions/foobar_nil_data/main.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/unittests/generic_functions/foobar_nil_data/main.py b/tests/unittests/generic_functions/foobar_nil_data/main.py index c806f5f51..a41823ddc 100644 --- a/tests/unittests/generic_functions/foobar_nil_data/main.py +++ b/tests/unittests/generic_functions/foobar_nil_data/main.py @@ -2,5 +2,6 @@ # Licensed under the MIT License. import logging + def main(input) -> None: logging.info("Hello World") From f714ea3ffa28a278ff703f0282e163cee699373c Mon Sep 17 00:00:00 2001 From: hallvictoria Date: Thu, 11 Jan 2024 10:41:21 -0600 Subject: [PATCH 4/9] added comment --- azure_functions_worker/bindings/generic.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/azure_functions_worker/bindings/generic.py b/azure_functions_worker/bindings/generic.py index a1b001519..07129ef6f 100644 --- a/azure_functions_worker/bindings/generic.py +++ b/azure_functions_worker/bindings/generic.py @@ -34,6 +34,8 @@ def encode(cls, obj: Any, *, @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 From b9d6d3126b509f3655d06877213d3cdd258076fb Mon Sep 17 00:00:00 2001 From: Victoria Hall Date: Wed, 27 Mar 2024 14:05:25 -0500 Subject: [PATCH 5/9] compatible with generic implicit output --- azure_functions_worker/bindings/datumdef.py | 2 ++ azure_functions_worker/bindings/generic.py | 5 ++++- 2 files changed, 6 insertions(+), 1 deletion(-) 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 b6b438027..28db5fe78 100644 --- a/azure_functions_worker/bindings/generic.py +++ b/azure_functions_worker/bindings/generic.py @@ -28,7 +28,8 @@ 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 @@ -46,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 ' From 66da8e229f0f74bfa8829e64a0a2bca5a951deba Mon Sep 17 00:00:00 2001 From: hallvictoria Date: Thu, 28 Mar 2024 14:15:18 -0500 Subject: [PATCH 6/9] removed if cond --- azure_functions_worker/bindings/meta.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) 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) From 3fd84f55eeaff50362f5a38dedc348a82030b90d Mon Sep 17 00:00:00 2001 From: hallvictoria Date: Fri, 29 Mar 2024 10:35:29 -0500 Subject: [PATCH 7/9] revert return none supp --- azure_functions_worker/bindings/datumdef.py | 2 -- azure_functions_worker/bindings/generic.py | 4 ---- azure_functions_worker/bindings/meta.py | 3 ++- tests/unittests/generic_functions/foobar_nil_data/main.py | 4 +++- tests/unittests/test_mock_generic_functions.py | 5 ++++- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/azure_functions_worker/bindings/datumdef.py b/azure_functions_worker/bindings/datumdef.py index 506221375..b420fbf3b 100644 --- a/azure_functions_worker/bindings/datumdef.py +++ b/azure_functions_worker/bindings/datumdef.py @@ -197,8 +197,6 @@ 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 28db5fe78..2975f4eb9 100644 --- a/azure_functions_worker/bindings/generic.py +++ b/azure_functions_worker/bindings/generic.py @@ -28,8 +28,6 @@ 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 @@ -47,8 +45,6 @@ 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 c00c2e97d..f7a810145 100644 --- a/azure_functions_worker/bindings/meta.py +++ b/azure_functions_worker/bindings/meta.py @@ -178,8 +178,9 @@ 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/unittests/generic_functions/foobar_nil_data/main.py b/tests/unittests/generic_functions/foobar_nil_data/main.py index a41823ddc..ac3e9ae6c 100644 --- a/tests/unittests/generic_functions/foobar_nil_data/main.py +++ b/tests/unittests/generic_functions/foobar_nil_data/main.py @@ -3,5 +3,7 @@ import logging -def main(input) -> None: +def main(input): logging.info("Hello World") + # The function must return a non-None value + return "This is fine" diff --git a/tests/unittests/test_mock_generic_functions.py b/tests/unittests/test_mock_generic_functions.py index e3e530e86..341b5d80c 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( @@ -219,4 +222,4 @@ async def test_mock_generic_as_nil_data(self): protos.StatusResult.Success) self.assertEqual( r.response.return_value, - protos.TypedData()) + protos.TypedData(string="This is fine")) From 40c105b76bcd8a13b62881c391fd532c828b973b Mon Sep 17 00:00:00 2001 From: hallvictoria Date: Tue, 2 Apr 2024 16:04:31 -0500 Subject: [PATCH 8/9] added back support for returning None --- azure_functions_worker/bindings/datumdef.py | 2 ++ azure_functions_worker/bindings/generic.py | 4 ++++ azure_functions_worker/bindings/meta.py | 3 +-- tests/unittests/generic_functions/foobar_nil_data/main.py | 4 +--- tests/unittests/test_mock_generic_functions.py | 2 +- 5 files changed, 9 insertions(+), 6 deletions(-) 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 2975f4eb9..28db5fe78 100644 --- a/azure_functions_worker/bindings/generic.py +++ b/azure_functions_worker/bindings/generic.py @@ -28,6 +28,8 @@ 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 @@ -45,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/unittests/generic_functions/foobar_nil_data/main.py b/tests/unittests/generic_functions/foobar_nil_data/main.py index ac3e9ae6c..a41823ddc 100644 --- a/tests/unittests/generic_functions/foobar_nil_data/main.py +++ b/tests/unittests/generic_functions/foobar_nil_data/main.py @@ -3,7 +3,5 @@ import logging -def main(input): +def main(input) -> None: logging.info("Hello World") - # The function must return a non-None value - return "This is fine" diff --git a/tests/unittests/test_mock_generic_functions.py b/tests/unittests/test_mock_generic_functions.py index 341b5d80c..695d53110 100644 --- a/tests/unittests/test_mock_generic_functions.py +++ b/tests/unittests/test_mock_generic_functions.py @@ -222,4 +222,4 @@ async def test_mock_generic_as_nil_data(self): protos.StatusResult.Success) self.assertEqual( r.response.return_value, - protos.TypedData(string="This is fine")) + protos.TypedData()) From 810f18de377541600e43eddf809f345122c5e67d Mon Sep 17 00:00:00 2001 From: hallvictoria Date: Thu, 18 Apr 2024 11:15:55 -0500 Subject: [PATCH 9/9] e2e test with generic bind and return none --- .../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 +++++++++++++ 4 files changed, 59 insertions(+) create mode 100644 tests/endtoend/generic_functions/return_none/function.json create mode 100644 tests/endtoend/generic_functions/return_none/main.py 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),