Skip to content

Commit

Permalink
allow return none for generic bindings (#1482)
Browse files Browse the repository at this point in the history
  • Loading branch information
hallvictoria authored Apr 18, 2024
1 parent 6427215 commit 3c0f473
Show file tree
Hide file tree
Showing 10 changed files with 114 additions and 3 deletions.
2 changes: 2 additions & 0 deletions azure_functions_worker/bindings/datumdef.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
9 changes: 8 additions & 1 deletion azure_functions_worker/bindings/generic.py
Original file line number Diff line number Diff line change
Expand Up @@ -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':
Expand All @@ -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 '
Expand Down
3 changes: 1 addition & 2 deletions azure_functions_worker/bindings/meta.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Original file line number Diff line number Diff line change
@@ -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)

Expand Down Expand Up @@ -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")
21 changes: 21 additions & 0 deletions tests/endtoend/generic_functions/return_none/function.json
Original file line number Diff line number Diff line change
@@ -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"
}
]
}
10 changes: 10 additions & 0 deletions tests/endtoend/generic_functions/return_none/main.py
Original file line number Diff line number Diff line change
@@ -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")
14 changes: 14 additions & 0 deletions tests/endtoend/test_generic_functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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),
Expand Down
10 changes: 10 additions & 0 deletions tests/unittests/generic_functions/foobar_nil_data/function.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"scriptFile": "main.py",
"bindings": [
{
"type": "generic",
"name": "input",
"direction": "in"
}
]
}
7 changes: 7 additions & 0 deletions tests/unittests/generic_functions/foobar_nil_data/main.py
Original file line number Diff line number Diff line change
@@ -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")
27 changes: 27 additions & 0 deletions tests/unittests/test_mock_generic_functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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())

0 comments on commit 3c0f473

Please sign in to comment.