Skip to content

Move implicit output bindings serialization logic to Python library #643

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 5 commits into from
Apr 23, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
10 changes: 8 additions & 2 deletions .ci/linux_devops_build.sh
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
#!/bin/bash

set -e -x


# Install the latest Azure Functions Python Worker from test.pypi.org
python -m pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple -U -e .[dev]
python setup.py webhost

# Install the latest Azure Functions Python Library from test.pypi.org
python -m pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple -U azure-functions --pre

# Download Azure Functions Host
python setup.py webhost
18 changes: 11 additions & 7 deletions azure_functions_worker/functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,8 @@ def add_function(self, function_id: str,
return_pytype: typing.Optional[type] = None

requires_context = False
has_return = False
has_explicit_return = False
has_implicit_return = False

bound_params = {}
for name, desc in metadata.bindings.items():
Expand All @@ -78,16 +79,16 @@ def add_function(self, function_id: str,
func_name,
f'"$return" binding must have direction set to "out"')

has_explicit_return = True
return_binding_name = desc.type
assert return_binding_name is not None

has_return = True
elif bindings.has_implicit_output(desc.type):
# If the binding specify implicit output binding
# (e.g. orchestrationTrigger, activityTrigger)
# we should enable output even if $return is not specified
has_return = True
return_binding_name = f'{desc.type}_ret'
has_implicit_return = True
return_binding_name = desc.type
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the line we refer to our trigger bindings (a.k.a. python-library activity-trigger) for serializing implicit output values.

bound_params[name] = desc
else:
bound_params[name] = desc
Expand Down Expand Up @@ -224,7 +225,7 @@ def add_function(self, function_id: str,
input_types[param.name] = param_type_info

return_pytype = None
if return_binding_name is not None and 'return' in annotations:
if has_explicit_return and 'return' in annotations:
return_anno = annotations.get('return')
if (typing_inspect.is_generic_type(return_anno)
and typing_inspect.get_origin(return_anno).__name__ == 'Out'):
Expand All @@ -249,8 +250,11 @@ def add_function(self, function_id: str,
f'Python return annotation "{return_pytype.__name__}" '
f'does not match binding type "{return_binding_name}"')

if has_implicit_return and 'return' in annotations:
return_pytype = annotations.get('return')

return_type = None
if return_binding_name is not None:
if has_explicit_return or has_implicit_return:
return_type = ParamTypeInfo(return_binding_name, return_pytype)

self._functions[function_id] = FunctionInfo(
Expand All @@ -259,7 +263,7 @@ def add_function(self, function_id: str,
directory=metadata.directory,
requires_context=requires_context,
is_async=inspect.iscoroutinefunction(func),
has_return=has_return,
has_return=has_explicit_return or has_implicit_return,
input_types=input_types,
output_types=output_types,
return_type=return_type)
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"scriptFile": "main.py",
"bindings": [
{
"type": "activityTrigger",
"name": "input",
"direction": "in"
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
from typing import Dict


def main(input: Dict[str, str]) -> Dict[str, str]:
result = input.copy()
if result.get('bird'):
result['bird'] = result['bird'][::-1]

return result
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"scriptFile": "main.py",
"bindings": [
{
"type": "activityTrigger",
"name": "input",
"direction": "in"
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
def main(input: int) -> float:
return float(input) * (-1.1)
73 changes: 68 additions & 5 deletions tests/unittests/test_mock_durable_functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,12 @@ async def test_mock_activity_trigger(self):

_, r = await host.invoke_function(
'activity_trigger', [
# According to Durable Python
# Activity Trigger's input must be json serializable
protos.ParameterBinding(
name='input',
data=protos.TypedData(
string='test'
string='test single_word'
)
)
]
Expand All @@ -29,7 +31,7 @@ async def test_mock_activity_trigger(self):
protos.StatusResult.Success)
self.assertEqual(
r.response.return_value,
protos.TypedData(string='test')
protos.TypedData(json='"test single_word"')
)

async def test_mock_activity_trigger_no_anno(self):
Expand All @@ -44,10 +46,12 @@ async def test_mock_activity_trigger_no_anno(self):

_, r = await host.invoke_function(
'activity_trigger_no_anno', [
# According to Durable Python
# Activity Trigger's input must be json serializable
protos.ParameterBinding(
name='input',
data=protos.TypedData(
bytes=b'\x34\x93\x04\x70'
string='test multiple words'
)
)
]
Expand All @@ -56,7 +60,66 @@ async def test_mock_activity_trigger_no_anno(self):
protos.StatusResult.Success)
self.assertEqual(
r.response.return_value,
protos.TypedData(bytes=b'\x34\x93\x04\x70')
protos.TypedData(json='"test multiple words"')
)

async def test_mock_activity_trigger_dict(self):
async with testutils.start_mockhost(
script_root=self.durable_functions_dir) as host:

func_id, r = await host.load_function('activity_trigger_dict')

self.assertEqual(r.response.function_id, func_id)
self.assertEqual(r.response.result.status,
protos.StatusResult.Success)

_, r = await host.invoke_function(
'activity_trigger_dict', [
# According to Durable Python
# Activity Trigger's input must be json serializable
protos.ParameterBinding(
name='input',
data=protos.TypedData(
json='{"bird": "Crane"}'
)
)
]
)
self.assertEqual(r.response.result.status,
protos.StatusResult.Success)
self.assertEqual(
r.response.return_value,
protos.TypedData(json='{"bird": "enarC"}')
)

async def test_mock_activity_trigger_int_to_float(self):
async with testutils.start_mockhost(
script_root=self.durable_functions_dir) as host:

func_id, r = await host.load_function(
'activity_trigger_int_to_float')

self.assertEqual(r.response.function_id, func_id)
self.assertEqual(r.response.result.status,
protos.StatusResult.Success)

_, r = await host.invoke_function(
'activity_trigger_int_to_float', [
# According to Durable Python
# Activity Trigger's input must be json serializable
protos.ParameterBinding(
name='input',
data=protos.TypedData(
json=str(int(10))
)
)
]
)
self.assertEqual(r.response.result.status,
protos.StatusResult.Success)
self.assertEqual(
r.response.return_value,
protos.TypedData(json='-11.0')
)

async def test_mock_orchestration_trigger(self):
Expand All @@ -83,5 +146,5 @@ async def test_mock_orchestration_trigger(self):
protos.StatusResult.Success)
self.assertEqual(
r.response.return_value,
protos.TypedData(string='Durable functions coming soon :)')
protos.TypedData(json='Durable functions coming soon :)')
)