Skip to content

Commit a754e58

Browse files
hallvictoriavrdmrgavin-aguiarVictoria Hall
authored
fix: allow returning none for generic bindings (Azure#1379)
* allow nill data for generic bindings * tests * lint * added comment * compatible with generic implicit output * removed if cond * revert return none supp * added back support for returning None * e2e test with generic bind and return none --------- Co-authored-by: Varad Meru <[email protected]> Co-authored-by: gavin-aguiar <[email protected]> Co-authored-by: Victoria Hall <[email protected]>
1 parent 51758a2 commit a754e58

File tree

10 files changed

+116
-3
lines changed

10 files changed

+116
-3
lines changed

Diff for: azure_functions_worker/bindings/datumdef.py

+2
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,8 @@ def datum_as_proto(datum: Datum) -> protos.TypedData:
199199
enable_content_negotiation=False,
200200
body=datum_as_proto(datum.value['body']),
201201
))
202+
elif datum.type is None:
203+
return None
202204
else:
203205
raise NotImplementedError(
204206
'unexpected Datum type: {!r}'.format(datum.type)

Diff for: azure_functions_worker/bindings/generic.py

+8-1
Original file line numberDiff line numberDiff line change
@@ -28,12 +28,17 @@ def encode(cls, obj: Any, *,
2828

2929
elif isinstance(obj, (bytes, bytearray)):
3030
return datumdef.Datum(type='bytes', value=bytes(obj))
31-
31+
elif obj is None:
32+
return datumdef.Datum(type=None, value=obj)
3233
else:
3334
raise NotImplementedError
3435

3536
@classmethod
3637
def decode(cls, data: datumdef.Datum, *, trigger_metadata) -> typing.Any:
38+
# Enabling support for Dapr bindings
39+
# https://github.com/Azure/azure-functions-python-worker/issues/1316
40+
if data is None:
41+
return None
3742
data_type = data.type
3843

3944
if data_type == 'string':
@@ -42,6 +47,8 @@ def decode(cls, data: datumdef.Datum, *, trigger_metadata) -> typing.Any:
4247
result = data.value
4348
elif data_type == 'json':
4449
result = data.value
50+
elif data_type is None:
51+
result = None
4552
else:
4653
raise ValueError(
4754
f'unexpected type of data received for the "generic" binding '

Diff for: azure_functions_worker/bindings/meta.py

+1-2
Original file line numberDiff line numberDiff line change
@@ -270,9 +270,8 @@ def to_outgoing_param_binding(binding: str, obj: typing.Any, *,
270270
rpc_shared_memory=shared_mem_value)
271271
else:
272272
# If not, send it as part of the response message over RPC
273+
# rpc_val can be None here as we now support a None return type
273274
rpc_val = datumdef.datum_as_proto(datum)
274-
if rpc_val is None:
275-
raise TypeError('Cannot convert datum to rpc_val')
276275
return protos.ParameterBinding(
277276
name=out_name,
278277
data=rpc_val)

Diff for: tests/endtoend/generic_functions/generic_functions_stein/function_app.py

+14
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# Copyright (c) Microsoft Corporation. All rights reserved.
22
# Licensed under the MIT License.
33
import azure.functions as func
4+
import logging
45

56
app = func.FunctionApp(http_auth_level=func.AuthLevel.ANONYMOUS)
67

@@ -29,3 +30,16 @@ def return_processed_last(req: func.HttpRequest, testEntity):
2930
table_name="EventHubBatchTest")
3031
def return_not_processed_last(req: func.HttpRequest, testEntities):
3132
return func.HttpResponse(status_code=200)
33+
34+
35+
@app.function_name(name="mytimer")
36+
@app.schedule(schedule="*/1 * * * * *", arg_name="mytimer",
37+
run_on_startup=False,
38+
use_monitor=False)
39+
@app.generic_input_binding(
40+
arg_name="testEntity",
41+
type="table",
42+
connection="AzureWebJobsStorage",
43+
table_name="EventHubBatchTest")
44+
def mytimer(mytimer: func.TimerRequest, testEntity) -> None:
45+
logging.info("This timer trigger function executed successfully")
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
{
2+
"scriptFile": "main.py",
3+
"bindings": [
4+
{
5+
"name": "mytimer",
6+
"type": "timerTrigger",
7+
"direction": "in",
8+
"schedule": "*/1 * * * * *",
9+
"runOnStartup": false
10+
},
11+
{
12+
"direction": "in",
13+
"type": "table",
14+
"name": "testEntity",
15+
"partitionKey": "test",
16+
"rowKey": "WillBePopulatedWithGuid",
17+
"tableName": "BindingTestTable",
18+
"connection": "AzureWebJobsStorage"
19+
}
20+
]
21+
}

Diff for: tests/endtoend/generic_functions/return_none/main.py

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# Copyright (c) Microsoft Corporation. All rights reserved.
2+
# Licensed under the MIT License.
3+
4+
import logging
5+
6+
import azure.functions as func
7+
8+
9+
def main(mytimer: func.TimerRequest, testEntity) -> None:
10+
logging.info("This timer trigger function executed successfully")

Diff for: tests/endtoend/test_generic_functions.py

+14
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22
# Licensed under the MIT License.
33
from unittest import skipIf
44

5+
import time
6+
import typing
7+
58
from azure_functions_worker.utils.common import is_envvar_true
69
from tests.utils import testutils
710
from tests.utils.constants import DEDICATED_DOCKER_TEST, CONSUMPTION_DOCKER_TEST
@@ -41,6 +44,17 @@ def test_return_not_processed_last(self):
4144
r = self.webhost.request('GET', 'return_not_processed_last')
4245
self.assertEqual(r.status_code, 200)
4346

47+
def test_return_none(self):
48+
time.sleep(1)
49+
# Checking webhost status.
50+
r = self.webhost.request('GET', '', no_prefix=True,
51+
timeout=5)
52+
self.assertTrue(r.ok)
53+
54+
def check_log_timer(self, host_out: typing.List[str]):
55+
self.assertEqual(host_out.count("This timer trigger function executed "
56+
"successfully"), 1)
57+
4458

4559
@skipIf(is_envvar_true(DEDICATED_DOCKER_TEST)
4660
or is_envvar_true(CONSUMPTION_DOCKER_TEST),
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"scriptFile": "main.py",
3+
"bindings": [
4+
{
5+
"type": "generic",
6+
"name": "input",
7+
"direction": "in"
8+
}
9+
]
10+
}
11+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# Copyright (c) Microsoft Corporation. All rights reserved.
2+
# Licensed under the MIT License.
3+
import logging
4+
5+
6+
def main(input) -> None:
7+
logging.info("Hello World")

Diff for: tests/unittests/test_mock_generic_functions.py

+28
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,9 @@ async def test_mock_generic_should_support_implicit_output(self):
144144
# implicitly
145145
self.assertEqual(r.response.result.status,
146146
protos.StatusResult.Success)
147+
self.assertEqual(
148+
r.response.return_value,
149+
protos.TypedData(bytes=b'\x00\x01'))
147150

148151
async def test_mock_generic_should_support_without_datatype(self):
149152
async with testutils.start_mockhost(
@@ -195,3 +198,28 @@ async def test_mock_generic_implicit_output_exemption(self):
195198
# For the Durable Functions durableClient case
196199
self.assertEqual(r.response.result.status,
197200
protos.StatusResult.Failure)
201+
202+
async def test_mock_generic_as_nil_data(self):
203+
async with testutils.start_mockhost(
204+
script_root=self.generic_funcs_dir) as host:
205+
206+
await host.init_worker("4.17.1")
207+
func_id, r = await host.load_function('foobar_nil_data')
208+
209+
self.assertEqual(r.response.function_id, func_id)
210+
self.assertEqual(r.response.result.status,
211+
protos.StatusResult.Success)
212+
213+
_, r = await host.invoke_function(
214+
'foobar_nil_data', [
215+
protos.ParameterBinding(
216+
name='input',
217+
data=protos.TypedData()
218+
)
219+
]
220+
)
221+
self.assertEqual(r.response.result.status,
222+
protos.StatusResult.Success)
223+
self.assertEqual(
224+
r.response.return_value,
225+
protos.TypedData())

0 commit comments

Comments
 (0)