Skip to content

Commit 9e70e3a

Browse files
authored
Move implicit output bindings serialization logic to Python library (Azure#643)
* Use json for serialization * Move implicit output binding serializer to python-library * Add unittests for primitive return types in activity triggers * Remove unnecessary comments in test cases * Fetch latest Azure Functions Python library from test.pypi.org
1 parent 751218f commit 9e70e3a

File tree

7 files changed

+118
-14
lines changed

7 files changed

+118
-14
lines changed

.ci/linux_devops_build.sh

+8-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
#!/bin/bash
22

33
set -e -x
4-
4+
5+
# Install the latest Azure Functions Python Worker from test.pypi.org
56
python -m pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple -U -e .[dev]
6-
python setup.py webhost
7+
8+
# Install the latest Azure Functions Python Library from test.pypi.org
9+
python -m pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple -U azure-functions --pre
10+
11+
# Download Azure Functions Host
12+
python setup.py webhost

azure_functions_worker/functions.py

+11-7
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,8 @@ def add_function(self, function_id: str,
6363
return_pytype: typing.Optional[type] = None
6464

6565
requires_context = False
66-
has_return = False
66+
has_explicit_return = False
67+
has_implicit_return = False
6768

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

82+
has_explicit_return = True
8183
return_binding_name = desc.type
8284
assert return_binding_name is not None
8385

84-
has_return = True
8586
elif bindings.has_implicit_output(desc.type):
8687
# If the binding specify implicit output binding
8788
# (e.g. orchestrationTrigger, activityTrigger)
8889
# we should enable output even if $return is not specified
89-
has_return = True
90-
return_binding_name = f'{desc.type}_ret'
90+
has_implicit_return = True
91+
return_binding_name = desc.type
9192
bound_params[name] = desc
9293
else:
9394
bound_params[name] = desc
@@ -224,7 +225,7 @@ def add_function(self, function_id: str,
224225
input_types[param.name] = param_type_info
225226

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

253+
if has_implicit_return and 'return' in annotations:
254+
return_pytype = annotations.get('return')
255+
252256
return_type = None
253-
if return_binding_name is not None:
257+
if has_explicit_return or has_implicit_return:
254258
return_type = ParamTypeInfo(return_binding_name, return_pytype)
255259

256260
self._functions[function_id] = FunctionInfo(
@@ -259,7 +263,7 @@ def add_function(self, function_id: str,
259263
directory=metadata.directory,
260264
requires_context=requires_context,
261265
is_async=inspect.iscoroutinefunction(func),
262-
has_return=has_return,
266+
has_return=has_explicit_return or has_implicit_return,
263267
input_types=input_types,
264268
output_types=output_types,
265269
return_type=return_type)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"scriptFile": "main.py",
3+
"bindings": [
4+
{
5+
"type": "activityTrigger",
6+
"name": "input",
7+
"direction": "in"
8+
}
9+
]
10+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
from typing import Dict
2+
3+
4+
def main(input: Dict[str, str]) -> Dict[str, str]:
5+
result = input.copy()
6+
if result.get('bird'):
7+
result['bird'] = result['bird'][::-1]
8+
9+
return result
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"scriptFile": "main.py",
3+
"bindings": [
4+
{
5+
"type": "activityTrigger",
6+
"name": "input",
7+
"direction": "in"
8+
}
9+
]
10+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
def main(input: int) -> float:
2+
return float(input) * (-1.1)

tests/unittests/test_mock_durable_functions.py

+68-5
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,12 @@ async def test_mock_activity_trigger(self):
1717

1818
_, r = await host.invoke_function(
1919
'activity_trigger', [
20+
# According to Durable Python
21+
# Activity Trigger's input must be json serializable
2022
protos.ParameterBinding(
2123
name='input',
2224
data=protos.TypedData(
23-
string='test'
25+
string='test single_word'
2426
)
2527
)
2628
]
@@ -29,7 +31,7 @@ async def test_mock_activity_trigger(self):
2931
protos.StatusResult.Success)
3032
self.assertEqual(
3133
r.response.return_value,
32-
protos.TypedData(string='test')
34+
protos.TypedData(json='"test single_word"')
3335
)
3436

3537
async def test_mock_activity_trigger_no_anno(self):
@@ -44,10 +46,12 @@ async def test_mock_activity_trigger_no_anno(self):
4446

4547
_, r = await host.invoke_function(
4648
'activity_trigger_no_anno', [
49+
# According to Durable Python
50+
# Activity Trigger's input must be json serializable
4751
protos.ParameterBinding(
4852
name='input',
4953
data=protos.TypedData(
50-
bytes=b'\x34\x93\x04\x70'
54+
string='test multiple words'
5155
)
5256
)
5357
]
@@ -56,7 +60,66 @@ async def test_mock_activity_trigger_no_anno(self):
5660
protos.StatusResult.Success)
5761
self.assertEqual(
5862
r.response.return_value,
59-
protos.TypedData(bytes=b'\x34\x93\x04\x70')
63+
protos.TypedData(json='"test multiple words"')
64+
)
65+
66+
async def test_mock_activity_trigger_dict(self):
67+
async with testutils.start_mockhost(
68+
script_root=self.durable_functions_dir) as host:
69+
70+
func_id, r = await host.load_function('activity_trigger_dict')
71+
72+
self.assertEqual(r.response.function_id, func_id)
73+
self.assertEqual(r.response.result.status,
74+
protos.StatusResult.Success)
75+
76+
_, r = await host.invoke_function(
77+
'activity_trigger_dict', [
78+
# According to Durable Python
79+
# Activity Trigger's input must be json serializable
80+
protos.ParameterBinding(
81+
name='input',
82+
data=protos.TypedData(
83+
json='{"bird": "Crane"}'
84+
)
85+
)
86+
]
87+
)
88+
self.assertEqual(r.response.result.status,
89+
protos.StatusResult.Success)
90+
self.assertEqual(
91+
r.response.return_value,
92+
protos.TypedData(json='{"bird": "enarC"}')
93+
)
94+
95+
async def test_mock_activity_trigger_int_to_float(self):
96+
async with testutils.start_mockhost(
97+
script_root=self.durable_functions_dir) as host:
98+
99+
func_id, r = await host.load_function(
100+
'activity_trigger_int_to_float')
101+
102+
self.assertEqual(r.response.function_id, func_id)
103+
self.assertEqual(r.response.result.status,
104+
protos.StatusResult.Success)
105+
106+
_, r = await host.invoke_function(
107+
'activity_trigger_int_to_float', [
108+
# According to Durable Python
109+
# Activity Trigger's input must be json serializable
110+
protos.ParameterBinding(
111+
name='input',
112+
data=protos.TypedData(
113+
json=str(int(10))
114+
)
115+
)
116+
]
117+
)
118+
self.assertEqual(r.response.result.status,
119+
protos.StatusResult.Success)
120+
self.assertEqual(
121+
r.response.return_value,
122+
protos.TypedData(json='-11.0')
60123
)
61124

62125
async def test_mock_orchestration_trigger(self):
@@ -83,5 +146,5 @@ async def test_mock_orchestration_trigger(self):
83146
protos.StatusResult.Success)
84147
self.assertEqual(
85148
r.response.return_value,
86-
protos.TypedData(string='Durable functions coming soon :)')
149+
protos.TypedData(json='Durable functions coming soon :)')
87150
)

0 commit comments

Comments
 (0)