Skip to content

Enabled implicit output for generic bindings #1391

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 26 commits into from
Mar 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
409ad9a
enabled implicit output for generic bindings
hallvictoria Jan 5, 2024
c2321a5
should only fail durable tests?
hallvictoria Jan 8, 2024
3a7bcc5
special case for table
hallvictoria Jan 8, 2024
9132411
extra test for table
hallvictoria Jan 8, 2024
5325c81
Merge branch 'dev' into hallvictoria/enable_generic_implicit_output
YunchuWang Jan 18, 2024
d20dac5
special case for logic apps
hallvictoria Jan 22, 2024
f3d3743
Merge branch 'hallvictoria/enable_generic_implicit_output' of https:/…
hallvictoria Jan 22, 2024
74ebb56
Merge branch 'dev' of https://github.com/Azure/azure-functions-python…
hallvictoria Jan 26, 2024
823375f
special case for durable
hallvictoria Jan 26, 2024
e1e9c1b
fixing failing eventhub batch test
hallvictoria Jan 30, 2024
b93b597
added output binding
hallvictoria Jan 31, 2024
8e4996c
flake
hallvictoria Jan 31, 2024
b864c31
eventhub output binding
hallvictoria Jan 31, 2024
05f89d4
flake
hallvictoria Jan 31, 2024
d3aa340
changed param name
hallvictoria Feb 5, 2024
0d8d3d9
add_function prio explicit return type
hallvictoria Feb 5, 2024
90d2651
Merge branch 'dev' into hallvictoria/enable_generic_implicit_output
hallvictoria Feb 5, 2024
d8e8ac1
added tests
hallvictoria Feb 6, 2024
0f5b223
Merge branch 'hallvictoria/enable_generic_implicit_output' of https:/…
hallvictoria Feb 6, 2024
751943f
added V2 test
hallvictoria Feb 8, 2024
a0541cd
remove if cond for durable client
hallvictoria Feb 12, 2024
76bfe95
lint
hallvictoria Feb 12, 2024
7b96bc3
Revert "remove if cond for durable client"
hallvictoria Feb 12, 2024
05b0659
Merge branch 'dev' into hallvictoria/enable_generic_implicit_output
hallvictoria Feb 29, 2024
cd9047c
Merge branch 'dev' into hallvictoria/enable_generic_implicit_output
hallvictoria Mar 4, 2024
466ee73
Merge branch 'dev' into hallvictoria/enable_generic_implicit_output
hallvictoria Mar 6, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions azure_functions_worker/bindings/generic.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,5 +51,7 @@ def decode(cls, data: datumdef.Datum, *, trigger_metadata) -> typing.Any:
return result

@classmethod
def has_implicit_output(cls) -> bool:
return False
def has_implicit_output(cls, bind_name: Optional[str]) -> bool:
if bind_name == 'durableClient':
return False
return True
12 changes: 9 additions & 3 deletions azure_functions_worker/bindings/meta.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,15 @@ def check_output_type_annotation(bind_name: str, pytype: type) -> bool:
def has_implicit_output(bind_name: str) -> bool:
binding = get_binding(bind_name)

# If the binding does not have metaclass of meta.InConverter
# The implicit_output does not exist
return getattr(binding, 'has_implicit_output', lambda: False)()
# Need to pass in bind_name to exempt Durable Functions
if binding is generic.GenericBinding:
return (getattr(binding, 'has_implicit_output', lambda: False)
(bind_name))

else:
# If the binding does not have metaclass of meta.InConverter
# The implicit_output does not exist
return getattr(binding, 'has_implicit_output', lambda: False)()


def from_incoming_proto(
Expand Down
28 changes: 20 additions & 8 deletions azure_functions_worker/functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,14 +71,20 @@ def get_explicit_and_implicit_return(binding_name: str,
@staticmethod
def get_return_binding(binding_name: str,
binding_type: str,
return_binding_name: str) -> str:
return_binding_name: str,
explicit_return_val_set: bool) \
-> typing.Tuple[str, bool]:
# prioritize explicit return value
if explicit_return_val_set:
return return_binding_name, explicit_return_val_set
if binding_name == "$return":
return_binding_name = binding_type
assert return_binding_name is not None
explicit_return_val_set = True
elif bindings_utils.has_implicit_output(binding_type):
return_binding_name = binding_type

return return_binding_name
return return_binding_name, explicit_return_val_set

@staticmethod
def validate_binding_direction(binding_name: str,
Expand Down Expand Up @@ -314,6 +320,7 @@ def add_function(self, function_id: str,
params = dict(sig.parameters)
annotations = typing.get_type_hints(func)
return_binding_name: typing.Optional[str] = None
explicit_return_val_set = False
has_explicit_return = False
has_implicit_return = False

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

return_binding_name = self.get_return_binding(binding_name,
binding_info.type,
return_binding_name)
return_binding_name, explicit_return_val_set = \
self.get_return_binding(binding_name,
binding_info.type,
return_binding_name,
explicit_return_val_set)

requires_context = self.is_context_required(params, bound_params,
annotations,
Expand Down Expand Up @@ -362,6 +371,7 @@ def add_indexed_function(self, function):
function_id = str(uuid.uuid5(namespace=uuid.NAMESPACE_OID,
name=func_name))
return_binding_name: typing.Optional[str] = None
explicit_return_val_set = False
has_explicit_return = False
has_implicit_return = False

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

return_binding_name = self.get_return_binding(binding.name,
binding.type,
return_binding_name)
return_binding_name, explicit_return_val_set = \
self.get_return_binding(binding.name,
binding.type,
return_binding_name,
explicit_return_val_set)

requires_context = self.is_context_required(params, bound_params,
annotations,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.
import azure.functions as func

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


@app.function_name(name="return_processed_last")
@app.generic_trigger(arg_name="req", type="httpTrigger",
route="return_processed_last")
@app.generic_output_binding(arg_name="$return", type="http")
@app.generic_input_binding(
arg_name="testEntity",
type="table",
connection="AzureWebJobsStorage",
table_name="EventHubBatchTest")
def return_processed_last(req: func.HttpRequest, testEntity):
return func.HttpResponse(status_code=200)


@app.function_name(name="return_not_processed_last")
@app.generic_trigger(arg_name="req", type="httpTrigger",
route="return_not_processed_last")
@app.generic_output_binding(arg_name="$return", type="http")
@app.generic_input_binding(
arg_name="testEntities",
type="table",
connection="AzureWebJobsStorage",
table_name="EventHubBatchTest")
def return_not_processed_last(req: func.HttpRequest, testEntities):
return func.HttpResponse(status_code=200)
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.
import azure.functions as func


# There are 3 bindings defined in function.json:
# 1. req: HTTP trigger
# 2. testEntities: table input (generic)
# 3. $return: HTTP response
# The bindings will be processed by the worker in this order:
# req -> $return -> testEntities
def main(req: func.HttpRequest, testEntities):
return func.HttpResponse(status_code=200)
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{
"scriptFile": "__init__.py",
"bindings": [
{
"type": "httpTrigger",
"direction": "in",
"authLevel": "anonymous",
"methods": [
"get"
],
"name": "req"
},
{
"direction": "in",
"type": "table",
"name": "testEntities",
"tableName": "EventHubBatchTest",
"connection": "AzureWebJobsStorage"
},
{
"type": "http",
"direction": "out",
"name": "$return"
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.
import azure.functions as func


# There are 3 bindings defined in function.json:
# 1. req: HTTP trigger
# 2. testEntity: table input (generic)
# 3. $return: HTTP response
# The bindings will be processed by the worker in this order:
# req -> testEntity -> $return
def main(req: func.HttpRequest, testEntity):
return func.HttpResponse(status_code=200)
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{
"scriptFile": "__init__.py",
"bindings": [
{
"type": "httpTrigger",
"direction": "in",
"authLevel": "anonymous",
"methods": [
"get"
],
"name": "req"
},
{
"direction": "in",
"type": "table",
"name": "testEntity",
"tableName": "EventHubBatchTest",
"connection": "AzureWebJobsStorage"
},
{
"type": "http",
"direction": "out",
"name": "$return"
}
]
}
54 changes: 54 additions & 0 deletions tests/endtoend/test_generic_functions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.
from unittest import skipIf

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


@skipIf(is_envvar_true(DEDICATED_DOCKER_TEST)
or is_envvar_true(CONSUMPTION_DOCKER_TEST),
"Table functions which are used in the bindings in these tests"
" has a bug with the table extension 1.0.0. "
"https://github.com/Azure/azure-sdk-for-net/issues/33902.")
class TestGenericFunctions(testutils.WebHostTestCase):
"""Test Generic Functions with implicit output enabled

With implicit output enabled for generic types, these tests cover
scenarios where a function has both explicit and implicit output
set to true. We prioritize explicit output. These tests check
that no matter the ordering, the return type is still correctly set.
"""

@classmethod
def get_script_dir(cls):
return testutils.E2E_TESTS_FOLDER / 'generic_functions'

def test_return_processed_last(self):
# Tests the case where implicit and explicit return are true
# in the same function and $return is processed before
# the generic binding is

r = self.webhost.request('GET', 'return_processed_last')
self.assertEqual(r.status_code, 200)

def test_return_not_processed_last(self):
# Tests the case where implicit and explicit return are true
# in the same function and the generic binding is processed
# before $return

r = self.webhost.request('GET', 'return_not_processed_last')
self.assertEqual(r.status_code, 200)


@skipIf(is_envvar_true(DEDICATED_DOCKER_TEST)
or is_envvar_true(CONSUMPTION_DOCKER_TEST),
"Table functions has a bug with the table extension 1.0.0."
"https://github.com/Azure/azure-sdk-for-net/issues/33902.")
class TestGenericFunctionsStein(TestGenericFunctions):

@classmethod
def get_script_dir(cls):
return testutils.E2E_TESTS_FOLDER / 'generic_functions' / \
'generic_functions_stein'
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"scriptFile": "main.py",
"bindings": [
{
"type": "durableClient",
"name": "input",
"direction": "in",
"dataType": "string"
}
]
}

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.
# Input as string, without annotation


def main(input: str):
return input
2 changes: 1 addition & 1 deletion tests/unittests/test_code_quality.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ def test_flake8(self):
try:
import flake8 # NoQA
except ImportError as e:
raise unittest.SkipTest('flake8 moudule is missing') from e
raise unittest.SkipTest('flake8 module is missing') from e

config_path = ROOT_PATH / '.flake8'
if not config_path.exists():
Expand Down
39 changes: 32 additions & 7 deletions tests/unittests/test_mock_generic_functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ async def test_mock_generic_as_bytes_no_anno(self):
protos.TypedData(bytes=b'\x00\x01')
)

async def test_mock_generic_should_not_support_implicit_output(self):
async def test_mock_generic_should_support_implicit_output(self):
async with testutils.start_mockhost(
script_root=self.generic_funcs_dir) as host:

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

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

async def test_mock_generic_should_support_without_datatype(self):
async with testutils.start_mockhost(
Expand All @@ -166,7 +166,32 @@ async def test_mock_generic_should_support_without_datatype(self):
)
]
)
# It should fail here, since the generic binding requires datatype
# to be defined in function.json
# It passes now as we are enabling generic binding to return output
# implicitly
self.assertEqual(r.response.result.status,
protos.StatusResult.Success)

async def test_mock_generic_implicit_output_exemption(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_implicit_output_exemption')
self.assertEqual(r.response.function_id, func_id)
self.assertEqual(r.response.result.status,
protos.StatusResult.Success)

_, r = await host.invoke_function(
'foobar_implicit_output_exemption', [
protos.ParameterBinding(
name='input',
data=protos.TypedData(
bytes=b'\x00\x01'
)
)
]
)
# It should fail here, since implicit output is False
# For the Durable Functions durableClient case
self.assertEqual(r.response.result.status,
protos.StatusResult.Failure)