Skip to content

Commit d3b416c

Browse files
authored
fix: update typing inspect get_origin check (#1691)
* update typing inspect * Fix test * lint * Fix for 3.9+ * Fix tests for 3.9+ * just add none check * lint * fix test
1 parent 181da8c commit d3b416c

File tree

5 files changed

+202
-83
lines changed

5 files changed

+202
-83
lines changed

workers/azure_functions_worker/functions.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -288,6 +288,7 @@ def get_function_return_type(annotations: dict, has_explicit_return: bool,
288288
return_anno = annotations.get('return')
289289
if typing_inspect.is_generic_type(
290290
return_anno) and typing_inspect.get_origin(
291+
return_anno) is not None and typing_inspect.get_origin(
291292
return_anno).__name__ == 'Out':
292293
raise FunctionLoadError(
293294
func_name,
Lines changed: 87 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,87 @@
1-
# Copyright (c) Microsoft Corporation. All rights reserved.
2-
# Licensed under the MIT License.
3-
4-
import logging
5-
import time
6-
from datetime import datetime
7-
8-
import azure.functions as func
9-
10-
app = func.FunctionApp(http_auth_level=func.AuthLevel.ANONYMOUS)
11-
12-
13-
@app.route(route="default_template")
14-
def default_template(req: func.HttpRequest) -> func.HttpResponse:
15-
logging.info('Python HTTP trigger function processed a request.')
16-
17-
name = req.params.get('name')
18-
if not name:
19-
try:
20-
req_body = req.get_json()
21-
except ValueError:
22-
pass
23-
else:
24-
name = req_body.get('name')
25-
26-
if name:
27-
return func.HttpResponse(
28-
f"Hello, {name}. This HTTP triggered function "
29-
f"executed successfully.")
30-
else:
31-
return func.HttpResponse(
32-
"This HTTP triggered function executed successfully. "
33-
"Pass a name in the query string or in the request body for a"
34-
" personalized response.",
35-
status_code=200
36-
)
37-
38-
39-
@app.route(route="http_func")
40-
def http_func(req: func.HttpRequest) -> func.HttpResponse:
41-
time.sleep(1)
42-
43-
current_time = datetime.now().strftime("%H:%M:%S")
44-
return func.HttpResponse(f"{current_time}")
1+
# Copyright (c) Microsoft Corporation. All rights reserved.
2+
# Licensed under the MIT License.
3+
4+
import json
5+
import logging
6+
import time
7+
8+
from datetime import datetime
9+
from typing import Generic, Mapping, Optional, TypeVar, Union
10+
11+
import azure.functions as func
12+
13+
app = func.FunctionApp(http_auth_level=func.AuthLevel.ANONYMOUS)
14+
15+
JsonType = Union[list, tuple, dict, str, int, float, bool]
16+
T = TypeVar("T", bound=JsonType)
17+
18+
19+
class JsonResponse(Generic[T], func.HttpResponse):
20+
def __init__(
21+
self,
22+
body: T,
23+
status_code: int = 200,
24+
headers: Optional[Mapping[str, str]] = None,
25+
):
26+
super().__init__(json.dumps(body),
27+
status_code=status_code,
28+
headers=headers,
29+
charset="utf-8")
30+
31+
32+
@app.route(route="default_template")
33+
def default_template(req: func.HttpRequest) -> func.HttpResponse:
34+
logging.info('Python HTTP trigger function processed a request.')
35+
36+
name = req.params.get('name')
37+
if not name:
38+
try:
39+
req_body = req.get_json()
40+
except ValueError:
41+
pass
42+
else:
43+
name = req_body.get('name')
44+
45+
if name:
46+
return func.HttpResponse(
47+
f"Hello, {name}. This HTTP triggered function "
48+
f"executed successfully.")
49+
else:
50+
return func.HttpResponse(
51+
"This HTTP triggered function executed successfully. "
52+
"Pass a name in the query string or in the request body for a"
53+
" personalized response.",
54+
status_code=200
55+
)
56+
57+
58+
@app.route(route="http_func")
59+
def http_func(req: func.HttpRequest) -> func.HttpResponse:
60+
time.sleep(1)
61+
62+
current_time = datetime.now().strftime("%H:%M:%S")
63+
return func.HttpResponse(f"{current_time}")
64+
65+
66+
@app.route(route="custom_response")
67+
def custom_response(req: func.HttpRequest) -> JsonResponse:
68+
name = req.params.get('name')
69+
if not name:
70+
try:
71+
req_body = req.get_json()
72+
except ValueError:
73+
pass
74+
else:
75+
name = req_body.get('name')
76+
if name:
77+
return JsonResponse(
78+
{
79+
"name": name
80+
},
81+
)
82+
else:
83+
return JsonResponse(
84+
{
85+
"status": "healthy"
86+
},
87+
)
Lines changed: 85 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,85 @@
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-
app = func.FunctionApp(http_auth_level=func.AuthLevel.ANONYMOUS)
9-
10-
11-
@app.function_name(name="default_template")
12-
@app.generic_trigger(arg_name="req",
13-
type="httpTrigger",
14-
route="default_template")
15-
@app.generic_output_binding(arg_name="$return", type="http")
16-
def default_template(req: func.HttpRequest) -> func.HttpResponse:
17-
logging.info('Python HTTP trigger function processed a request.')
18-
19-
name = req.params.get('name')
20-
if not name:
21-
try:
22-
req_body = req.get_json()
23-
except ValueError:
24-
pass
25-
else:
26-
name = req_body.get('name')
27-
28-
if name:
29-
return func.HttpResponse(
30-
f"Hello, {name}. This HTTP triggered function "
31-
f"executed successfully.")
32-
else:
33-
return func.HttpResponse(
34-
"This HTTP triggered function executed successfully. "
35-
"Pass a name in the query string or in the request body for a"
36-
" personalized response.",
37-
status_code=200
38-
)
1+
# Copyright (c) Microsoft Corporation. All rights reserved.
2+
# Licensed under the MIT License.
3+
4+
import json
5+
import logging
6+
7+
from typing import Generic, Mapping, Optional, TypeVar, Union
8+
9+
import azure.functions as func
10+
11+
app = func.FunctionApp(http_auth_level=func.AuthLevel.ANONYMOUS)
12+
13+
14+
JsonType = Union[list, tuple, dict, str, int, float, bool]
15+
T = TypeVar("T", bound=JsonType)
16+
17+
18+
class JsonResponse(Generic[T], func.HttpResponse):
19+
def __init__(
20+
self,
21+
body: T,
22+
status_code: int = 200,
23+
headers: Optional[Mapping[str, str]] = None,
24+
):
25+
super().__init__(json.dumps(body),
26+
status_code=status_code,
27+
headers=headers,
28+
charset="utf-8")
29+
30+
31+
@app.function_name(name="default_template")
32+
@app.generic_trigger(arg_name="req",
33+
type="httpTrigger",
34+
route="default_template")
35+
@app.generic_output_binding(arg_name="$return", type="http")
36+
def default_template(req: func.HttpRequest) -> func.HttpResponse:
37+
logging.info('Python HTTP trigger function processed a request.')
38+
39+
name = req.params.get('name')
40+
if not name:
41+
try:
42+
req_body = req.get_json()
43+
except ValueError:
44+
pass
45+
else:
46+
name = req_body.get('name')
47+
48+
if name:
49+
return func.HttpResponse(
50+
f"Hello, {name}. This HTTP triggered function "
51+
f"executed successfully.")
52+
else:
53+
return func.HttpResponse(
54+
"This HTTP triggered function executed successfully. "
55+
"Pass a name in the query string or in the request body for a"
56+
" personalized response.",
57+
status_code=200
58+
)
59+
60+
61+
@app.generic_trigger(arg_name="req",
62+
type="httpTrigger",
63+
route="custom_response")
64+
@app.generic_output_binding(arg_name="$return", type="http")
65+
def custom_response(req: func.HttpRequest) -> JsonResponse:
66+
name = req.params.get('name')
67+
if not name:
68+
try:
69+
req_body = req.get_json()
70+
except ValueError:
71+
pass
72+
else:
73+
name = req_body.get('name')
74+
if name:
75+
return JsonResponse(
76+
{
77+
"name": name
78+
},
79+
)
80+
else:
81+
return JsonResponse(
82+
{
83+
"status": "healthy"
84+
},
85+
)

workers/tests/endtoend/test_http_functions.py

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,8 +118,33 @@ def get_script_dir(cls):
118118
return testutils.E2E_TESTS_FOLDER / 'http_functions' / \
119119
'http_functions_stein'
120120

121+
@testutils.retryable_test(3, 5)
122+
def test_return_custom_class(self):
123+
"""Test if returning a custom class returns OK
124+
"""
125+
r = self.webhost.request('GET', 'custom_response',
126+
timeout=REQUEST_TIMEOUT_SEC)
127+
self.assertEqual(
128+
r.content,
129+
b'{"status": "healthy"}'
130+
)
131+
self.assertTrue(r.ok)
132+
133+
@testutils.retryable_test(3, 5)
134+
def test_return_custom_class_with_query_param(self):
135+
"""Test if query is accepted
136+
"""
137+
r = self.webhost.request('GET', 'custom_response',
138+
params={'name': 'query'},
139+
timeout=REQUEST_TIMEOUT_SEC)
140+
self.assertTrue(r.ok)
141+
self.assertEqual(
142+
r.content,
143+
b'{"name": "query"}'
144+
)
145+
121146

122-
class TestHttpFunctionsSteinGeneric(TestHttpFunctions):
147+
class TestHttpFunctionsSteinGeneric(TestHttpFunctionsStein):
123148

124149
@classmethod
125150
def get_script_dir(cls):

workers/tests/unittests/test_typing_inspect.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,11 +98,14 @@ class GetUtilityTestCase(TestCase):
9898

9999
def test_origin(self):
100100
T = TypeVar('T')
101+
class MyClass(Generic[T]): pass
102+
101103
self.assertEqual(get_origin(int), None)
102104
self.assertEqual(get_origin(ClassVar[int]), None)
103105
self.assertEqual(get_origin(Generic), Generic)
104106
self.assertEqual(get_origin(Generic[T]), Generic)
105107
self.assertEqual(get_origin(List[Tuple[T, T]][int]), list)
108+
self.assertEqual(get_origin(MyClass), None)
106109

107110
def test_parameters(self):
108111
T = TypeVar('T')

0 commit comments

Comments
 (0)