Skip to content

Commit c7ab3e9

Browse files
Enabled implicit output for generic bindings (#1391)
* enabled implicit output for generic bindings * should only fail durable tests? * special case for table * extra test for table * special case for logic apps * special case for durable * fixing failing eventhub batch test * added output binding * flake * eventhub output binding * flake * changed param name * add_function prio explicit return type * added tests * added V2 test * remove if cond for durable client * lint * Revert "remove if cond for durable client" --------- Co-authored-by: peterstone2017 <12449837+YunchuWang@users.noreply.github.com>
1 parent 2e91fa1 commit c7ab3e9

File tree

13 files changed

+248
-21
lines changed

13 files changed

+248
-21
lines changed

azure_functions_worker/bindings/generic.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,5 +51,7 @@ def decode(cls, data: datumdef.Datum, *, trigger_metadata) -> typing.Any:
5151
return result
5252

5353
@classmethod
54-
def has_implicit_output(cls) -> bool:
55-
return False
54+
def has_implicit_output(cls, bind_name: Optional[str]) -> bool:
55+
if bind_name == 'durableClient':
56+
return False
57+
return True

azure_functions_worker/bindings/meta.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -55,9 +55,15 @@ def check_output_type_annotation(bind_name: str, pytype: type) -> bool:
5555
def has_implicit_output(bind_name: str) -> bool:
5656
binding = get_binding(bind_name)
5757

58-
# If the binding does not have metaclass of meta.InConverter
59-
# The implicit_output does not exist
60-
return getattr(binding, 'has_implicit_output', lambda: False)()
58+
# Need to pass in bind_name to exempt Durable Functions
59+
if binding is generic.GenericBinding:
60+
return (getattr(binding, 'has_implicit_output', lambda: False)
61+
(bind_name))
62+
63+
else:
64+
# If the binding does not have metaclass of meta.InConverter
65+
# The implicit_output does not exist
66+
return getattr(binding, 'has_implicit_output', lambda: False)()
6167

6268

6369
def from_incoming_proto(

azure_functions_worker/functions.py

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -71,14 +71,20 @@ def get_explicit_and_implicit_return(binding_name: str,
7171
@staticmethod
7272
def get_return_binding(binding_name: str,
7373
binding_type: str,
74-
return_binding_name: str) -> str:
74+
return_binding_name: str,
75+
explicit_return_val_set: bool) \
76+
-> typing.Tuple[str, bool]:
77+
# prioritize explicit return value
78+
if explicit_return_val_set:
79+
return return_binding_name, explicit_return_val_set
7580
if binding_name == "$return":
7681
return_binding_name = binding_type
7782
assert return_binding_name is not None
83+
explicit_return_val_set = True
7884
elif bindings_utils.has_implicit_output(binding_type):
7985
return_binding_name = binding_type
8086

81-
return return_binding_name
87+
return return_binding_name, explicit_return_val_set
8288

8389
@staticmethod
8490
def validate_binding_direction(binding_name: str,
@@ -314,6 +320,7 @@ def add_function(self, function_id: str,
314320
params = dict(sig.parameters)
315321
annotations = typing.get_type_hints(func)
316322
return_binding_name: typing.Optional[str] = None
323+
explicit_return_val_set = False
317324
has_explicit_return = False
318325
has_implicit_return = False
319326

@@ -327,9 +334,11 @@ def add_function(self, function_id: str,
327334
binding_name, binding_info, has_explicit_return,
328335
has_implicit_return, bound_params)
329336

330-
return_binding_name = self.get_return_binding(binding_name,
331-
binding_info.type,
332-
return_binding_name)
337+
return_binding_name, explicit_return_val_set = \
338+
self.get_return_binding(binding_name,
339+
binding_info.type,
340+
return_binding_name,
341+
explicit_return_val_set)
333342

334343
requires_context = self.is_context_required(params, bound_params,
335344
annotations,
@@ -362,6 +371,7 @@ def add_indexed_function(self, function):
362371
function_id = str(uuid.uuid5(namespace=uuid.NAMESPACE_OID,
363372
name=func_name))
364373
return_binding_name: typing.Optional[str] = None
374+
explicit_return_val_set = False
365375
has_explicit_return = False
366376
has_implicit_return = False
367377

@@ -381,9 +391,11 @@ def add_indexed_function(self, function):
381391
binding.name, binding, has_explicit_return,
382392
has_implicit_return, bound_params)
383393

384-
return_binding_name = self.get_return_binding(binding.name,
385-
binding.type,
386-
return_binding_name)
394+
return_binding_name, explicit_return_val_set = \
395+
self.get_return_binding(binding.name,
396+
binding.type,
397+
return_binding_name,
398+
explicit_return_val_set)
387399

388400
requires_context = self.is_context_required(params, bound_params,
389401
annotations,
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# Copyright (c) Microsoft Corporation. All rights reserved.
2+
# Licensed under the MIT License.
3+
import azure.functions as func
4+
5+
app = func.FunctionApp(http_auth_level=func.AuthLevel.ANONYMOUS)
6+
7+
8+
@app.function_name(name="return_processed_last")
9+
@app.generic_trigger(arg_name="req", type="httpTrigger",
10+
route="return_processed_last")
11+
@app.generic_output_binding(arg_name="$return", type="http")
12+
@app.generic_input_binding(
13+
arg_name="testEntity",
14+
type="table",
15+
connection="AzureWebJobsStorage",
16+
table_name="EventHubBatchTest")
17+
def return_processed_last(req: func.HttpRequest, testEntity):
18+
return func.HttpResponse(status_code=200)
19+
20+
21+
@app.function_name(name="return_not_processed_last")
22+
@app.generic_trigger(arg_name="req", type="httpTrigger",
23+
route="return_not_processed_last")
24+
@app.generic_output_binding(arg_name="$return", type="http")
25+
@app.generic_input_binding(
26+
arg_name="testEntities",
27+
type="table",
28+
connection="AzureWebJobsStorage",
29+
table_name="EventHubBatchTest")
30+
def return_not_processed_last(req: func.HttpRequest, testEntities):
31+
return func.HttpResponse(status_code=200)
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# Copyright (c) Microsoft Corporation. All rights reserved.
2+
# Licensed under the MIT License.
3+
import azure.functions as func
4+
5+
6+
# There are 3 bindings defined in function.json:
7+
# 1. req: HTTP trigger
8+
# 2. testEntities: table input (generic)
9+
# 3. $return: HTTP response
10+
# The bindings will be processed by the worker in this order:
11+
# req -> $return -> testEntities
12+
def main(req: func.HttpRequest, testEntities):
13+
return func.HttpResponse(status_code=200)
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
{
2+
"scriptFile": "__init__.py",
3+
"bindings": [
4+
{
5+
"type": "httpTrigger",
6+
"direction": "in",
7+
"authLevel": "anonymous",
8+
"methods": [
9+
"get"
10+
],
11+
"name": "req"
12+
},
13+
{
14+
"direction": "in",
15+
"type": "table",
16+
"name": "testEntities",
17+
"tableName": "EventHubBatchTest",
18+
"connection": "AzureWebJobsStorage"
19+
},
20+
{
21+
"type": "http",
22+
"direction": "out",
23+
"name": "$return"
24+
}
25+
]
26+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# Copyright (c) Microsoft Corporation. All rights reserved.
2+
# Licensed under the MIT License.
3+
import azure.functions as func
4+
5+
6+
# There are 3 bindings defined in function.json:
7+
# 1. req: HTTP trigger
8+
# 2. testEntity: table input (generic)
9+
# 3. $return: HTTP response
10+
# The bindings will be processed by the worker in this order:
11+
# req -> testEntity -> $return
12+
def main(req: func.HttpRequest, testEntity):
13+
return func.HttpResponse(status_code=200)
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
{
2+
"scriptFile": "__init__.py",
3+
"bindings": [
4+
{
5+
"type": "httpTrigger",
6+
"direction": "in",
7+
"authLevel": "anonymous",
8+
"methods": [
9+
"get"
10+
],
11+
"name": "req"
12+
},
13+
{
14+
"direction": "in",
15+
"type": "table",
16+
"name": "testEntity",
17+
"tableName": "EventHubBatchTest",
18+
"connection": "AzureWebJobsStorage"
19+
},
20+
{
21+
"type": "http",
22+
"direction": "out",
23+
"name": "$return"
24+
}
25+
]
26+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
# Copyright (c) Microsoft Corporation. All rights reserved.
2+
# Licensed under the MIT License.
3+
from unittest import skipIf
4+
5+
from azure_functions_worker.utils.common import is_envvar_true
6+
from tests.utils import testutils
7+
from tests.utils.constants import DEDICATED_DOCKER_TEST, CONSUMPTION_DOCKER_TEST
8+
9+
10+
@skipIf(is_envvar_true(DEDICATED_DOCKER_TEST)
11+
or is_envvar_true(CONSUMPTION_DOCKER_TEST),
12+
"Table functions which are used in the bindings in these tests"
13+
" has a bug with the table extension 1.0.0. "
14+
"https://github.com/Azure/azure-sdk-for-net/issues/33902.")
15+
class TestGenericFunctions(testutils.WebHostTestCase):
16+
"""Test Generic Functions with implicit output enabled
17+
18+
With implicit output enabled for generic types, these tests cover
19+
scenarios where a function has both explicit and implicit output
20+
set to true. We prioritize explicit output. These tests check
21+
that no matter the ordering, the return type is still correctly set.
22+
"""
23+
24+
@classmethod
25+
def get_script_dir(cls):
26+
return testutils.E2E_TESTS_FOLDER / 'generic_functions'
27+
28+
def test_return_processed_last(self):
29+
# Tests the case where implicit and explicit return are true
30+
# in the same function and $return is processed before
31+
# the generic binding is
32+
33+
r = self.webhost.request('GET', 'return_processed_last')
34+
self.assertEqual(r.status_code, 200)
35+
36+
def test_return_not_processed_last(self):
37+
# Tests the case where implicit and explicit return are true
38+
# in the same function and the generic binding is processed
39+
# before $return
40+
41+
r = self.webhost.request('GET', 'return_not_processed_last')
42+
self.assertEqual(r.status_code, 200)
43+
44+
45+
@skipIf(is_envvar_true(DEDICATED_DOCKER_TEST)
46+
or is_envvar_true(CONSUMPTION_DOCKER_TEST),
47+
"Table functions has a bug with the table extension 1.0.0."
48+
"https://github.com/Azure/azure-sdk-for-net/issues/33902.")
49+
class TestGenericFunctionsStein(TestGenericFunctions):
50+
51+
@classmethod
52+
def get_script_dir(cls):
53+
return testutils.E2E_TESTS_FOLDER / 'generic_functions' / \
54+
'generic_functions_stein'
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"scriptFile": "main.py",
3+
"bindings": [
4+
{
5+
"type": "durableClient",
6+
"name": "input",
7+
"direction": "in",
8+
"dataType": "string"
9+
}
10+
]
11+
}
12+
Lines changed: 7 additions & 0 deletions
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+
# Input as string, without annotation
4+
5+
6+
def main(input: str):
7+
return input

tests/unittests/test_code_quality.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ def test_flake8(self):
3636
try:
3737
import flake8 # NoQA
3838
except ImportError as e:
39-
raise unittest.SkipTest('flake8 moudule is missing') from e
39+
raise unittest.SkipTest('flake8 module is missing') from e
4040

4141
config_path = ROOT_PATH / '.flake8'
4242
if not config_path.exists():

tests/unittests/test_mock_generic_functions.py

Lines changed: 32 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ async def test_mock_generic_as_bytes_no_anno(self):
119119
protos.TypedData(bytes=b'\x00\x01')
120120
)
121121

122-
async def test_mock_generic_should_not_support_implicit_output(self):
122+
async def test_mock_generic_should_support_implicit_output(self):
123123
async with testutils.start_mockhost(
124124
script_root=self.generic_funcs_dir) as host:
125125

@@ -131,7 +131,7 @@ async def test_mock_generic_should_not_support_implicit_output(self):
131131
protos.StatusResult.Success)
132132

133133
_, r = await host.invoke_function(
134-
'foobar_as_bytes_no_anno', [
134+
'foobar_implicit_output', [
135135
protos.ParameterBinding(
136136
name='input',
137137
data=protos.TypedData(
@@ -140,10 +140,10 @@ async def test_mock_generic_should_not_support_implicit_output(self):
140140
)
141141
]
142142
)
143-
# It should fail here, since generic binding requires
144-
# $return statement in function.json to pass output
143+
# It passes now as we are enabling generic binding to return output
144+
# implicitly
145145
self.assertEqual(r.response.result.status,
146-
protos.StatusResult.Failure)
146+
protos.StatusResult.Success)
147147

148148
async def test_mock_generic_should_support_without_datatype(self):
149149
async with testutils.start_mockhost(
@@ -166,7 +166,32 @@ async def test_mock_generic_should_support_without_datatype(self):
166166
)
167167
]
168168
)
169-
# It should fail here, since the generic binding requires datatype
170-
# to be defined in function.json
169+
# It passes now as we are enabling generic binding to return output
170+
# implicitly
171+
self.assertEqual(r.response.result.status,
172+
protos.StatusResult.Success)
173+
174+
async def test_mock_generic_implicit_output_exemption(self):
175+
async with testutils.start_mockhost(
176+
script_root=self.generic_funcs_dir) as host:
177+
await host.init_worker("4.17.1")
178+
func_id, r = await host.load_function(
179+
'foobar_implicit_output_exemption')
180+
self.assertEqual(r.response.function_id, func_id)
181+
self.assertEqual(r.response.result.status,
182+
protos.StatusResult.Success)
183+
184+
_, r = await host.invoke_function(
185+
'foobar_implicit_output_exemption', [
186+
protos.ParameterBinding(
187+
name='input',
188+
data=protos.TypedData(
189+
bytes=b'\x00\x01'
190+
)
191+
)
192+
]
193+
)
194+
# It should fail here, since implicit output is False
195+
# For the Durable Functions durableClient case
171196
self.assertEqual(r.response.result.status,
172197
protos.StatusResult.Failure)

0 commit comments

Comments
 (0)