Skip to content

Commit 656a404

Browse files
authored
Merge branch 'dev' into hallvictoria/e2e-test-fix
2 parents 1305ed8 + 6587898 commit 656a404

File tree

16 files changed

+164
-18
lines changed

16 files changed

+164
-18
lines changed

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)

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 '

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)

azure_functions_worker/constants.py

+3
Original file line numberDiff line numberDiff line change
@@ -80,3 +80,6 @@
8080

8181
# Base extension supported Python minor version
8282
BASE_EXT_SUPPORTED_PY_MINOR_VERSION = 8
83+
84+
PYTHON_ENABLE_OPENTELEMETRY = "PYTHON_ENABLE_OPENTELEMETRY"
85+
PYTHON_ENABLE_OPENTELEMETRY_DEFAULT = True

azure_functions_worker/dispatcher.py

+25-12
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,9 @@
3030
PYTHON_SCRIPT_FILE_NAME,
3131
PYTHON_SCRIPT_FILE_NAME_DEFAULT,
3232
PYTHON_LANGUAGE_RUNTIME, PYTHON_ENABLE_INIT_INDEXING,
33-
METADATA_PROPERTIES_WORKER_INDEXED)
33+
METADATA_PROPERTIES_WORKER_INDEXED,
34+
PYTHON_ENABLE_OPENTELEMETRY,
35+
PYTHON_ENABLE_OPENTELEMETRY_DEFAULT)
3436
from .extension import ExtensionManager
3537
from .http_v2 import http_coordinator, initialize_http_server, HttpV2Registry, \
3638
sync_http_request, HttpServerInitError
@@ -318,10 +320,12 @@ async def _handle__worker_init_request(self, request):
318320
constants.SHARED_MEMORY_DATA_TRANSFER: _TRUE,
319321
}
320322

321-
self.update_opentelemetry_status()
323+
if get_app_setting(setting=PYTHON_ENABLE_OPENTELEMETRY,
324+
default_value=PYTHON_ENABLE_OPENTELEMETRY_DEFAULT):
325+
self.update_opentelemetry_status()
322326

323-
if self._otel_libs_available:
324-
capabilities[constants.WORKER_OPEN_TELEMETRY_ENABLED] = _TRUE
327+
if self._otel_libs_available:
328+
capabilities[constants.WORKER_OPEN_TELEMETRY_ENABLED] = _TRUE
325329

326330
if DependencyManager.should_load_cx_dependencies():
327331
DependencyManager.prioritize_customer_dependencies()
@@ -383,8 +387,10 @@ def load_function_metadata(self, function_app_directory, caller_info):
383387
function_path = os.path.join(function_app_directory,
384388
script_file_name)
385389

390+
# For V1, the function path will not exist and
391+
# return None.
386392
self._function_metadata_result = (
387-
self.index_functions(function_path)) \
393+
self.index_functions(function_path, function_app_directory)) \
388394
if os.path.exists(function_path) else None
389395

390396
async def _handle__functions_metadata_request(self, request):
@@ -439,8 +445,9 @@ async def _handle__function_load_request(self, request):
439445

440446
logger.info(
441447
'Received WorkerLoadRequest, request ID %s, function_id: %s,'
442-
'function_name: %s',
443-
self.request_id, function_id, function_name)
448+
'function_name: %s, function_app_directory : %s',
449+
self.request_id, function_id, function_name,
450+
function_app_directory)
444451

445452
programming_model = "V2"
446453
try:
@@ -705,9 +712,14 @@ async def _handle__function_environment_reload_request(self, request):
705712
bindings.load_binding_registry()
706713

707714
capabilities = {}
708-
self.update_opentelemetry_status()
709-
if self._otel_libs_available:
710-
capabilities[constants.WORKER_OPEN_TELEMETRY_ENABLED] = _TRUE
715+
if get_app_setting(
716+
setting=PYTHON_ENABLE_OPENTELEMETRY,
717+
default_value=PYTHON_ENABLE_OPENTELEMETRY_DEFAULT):
718+
self.update_opentelemetry_status()
719+
720+
if self._otel_libs_available:
721+
capabilities[constants.WORKER_OPEN_TELEMETRY_ENABLED] = (
722+
_TRUE)
711723

712724
if is_envvar_true(PYTHON_ENABLE_INIT_INDEXING):
713725
try:
@@ -749,7 +761,7 @@ async def _handle__function_environment_reload_request(self, request):
749761
request_id=self.request_id,
750762
function_environment_reload_response=failure_response)
751763

752-
def index_functions(self, function_path: str):
764+
def index_functions(self, function_path: str, function_dir: str):
753765
indexed_functions = loader.index_function_app(function_path)
754766
logger.info(
755767
"Indexed function app and found %s functions",
@@ -760,7 +772,8 @@ def index_functions(self, function_path: str):
760772
fx_metadata_results, fx_bindings_logs = (
761773
loader.process_indexed_function(
762774
self._functions,
763-
indexed_functions))
775+
indexed_functions,
776+
function_dir))
764777

765778
indexed_function_logs: List[str] = []
766779
indexed_function_bindings_logs = []

azure_functions_worker/loader.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ def build_variable_interval_retry(retry, max_retry_count, retry_strategy):
121121

122122

123123
def process_indexed_function(functions_registry: functions.Registry,
124-
indexed_functions):
124+
indexed_functions, function_dir):
125125
"""
126126
fx_metadata_results is a list of the RpcFunctionMetadata for
127127
all the functions in the particular app.
@@ -150,7 +150,7 @@ def process_indexed_function(functions_registry: functions.Registry,
150150
name=function_info.name,
151151
function_id=function_info.function_id,
152152
managed_dependency_enabled=False, # only enabled for PowerShell
153-
directory=function_info.directory,
153+
directory=function_dir,
154154
script_file=indexed_function.function_script_file,
155155
entry_point=function_info.name,
156156
is_proxy=False, # not supported in V4

tests/endtoend/blueprint_functions/functions_in_blueprint_only/blueprint.py

+10
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import logging
2+
import time
3+
from datetime import datetime
24

35
import azure.functions as func
46

@@ -29,3 +31,11 @@ def default_template(req: func.HttpRequest) -> func.HttpResponse:
2931
" personalized response.",
3032
status_code=200
3133
)
34+
35+
36+
@bp.route(route="http_func")
37+
def http_func(req: func.HttpRequest) -> func.HttpResponse:
38+
time.sleep(1)
39+
40+
current_time = datetime.now().strftime("%H:%M:%S")
41+
return func.HttpResponse(f"{current_time}")

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+
}
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")

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),

tests/endtoend/test_worker_process_count_functions.py

+8
Original file line numberDiff line numberDiff line change
@@ -71,3 +71,11 @@ class TestWorkerProcessCountStein(TestWorkerProcessCount):
7171
def get_script_dir(cls):
7272
return testutils.E2E_TESTS_FOLDER / 'http_functions' /\
7373
'http_functions_stein'
74+
75+
76+
class TestWorkerProcessCountWithBlueprintStein(TestWorkerProcessCount):
77+
78+
@classmethod
79+
def get_script_dir(cls):
80+
return testutils.E2E_TESTS_FOLDER / 'blueprint_functions' /\
81+
'functions_in_blueprint_only'
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")

tests/unittests/test_dispatcher.py

-1
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@
2121
from tests.utils import testutils
2222
from tests.utils.testutils import UNIT_TESTS_ROOT
2323

24-
2524
SysVersionInfo = col.namedtuple("VersionInfo", ["major", "minor", "micro",
2625
"releaselevel", "serial"])
2726
DISPATCHER_FUNCTIONS_DIR = testutils.UNIT_TESTS_FOLDER / 'dispatcher_functions'

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)