Skip to content

Commit 1709e9f

Browse files
authored
Http V2 (Part 1): Add req sychroznier and built in resp types (#23)
* linting * add httpv2 req synchronizer and support built int resp types * more cov * more * fb * test * no cover
1 parent 387f907 commit 1709e9f

File tree

3 files changed

+191
-12
lines changed

3 files changed

+191
-12
lines changed

azure-functions-extension-base/azure/functions/extension/base/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
from .web import (
1414
HttpV2FeatureChecker,
1515
ModuleTrackerMeta,
16+
RequestSynchronizer,
1617
RequestTrackerMeta,
1718
ResponseLabels,
1819
ResponseTrackerMeta,
@@ -35,6 +36,7 @@
3536
"ResponseLabels",
3637
"WebServer",
3738
"WebApp",
39+
"RequestSynchronizer",
3840
]
3941

4042
__version__ = "1.0.0a2"

azure-functions-extension-base/azure/functions/extension/base/web.py

Lines changed: 37 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import abc
12
import inspect
23
from abc import abstractmethod
34
from enum import Enum
@@ -34,28 +35,37 @@ def module_imported(cls):
3435

3536
class RequestTrackerMeta(type):
3637
_request_type = None
38+
_synchronizer: None
3739

3840
def __new__(cls, name, bases, dct, **kwargs):
3941
new_class = super().__new__(cls, name, bases, dct)
4042

4143
request_type = dct.get("request_type")
4244

4345
if request_type is None:
44-
raise Exception(f"Request type not provided for class {name}")
46+
raise TypeError(f"Request type not provided for class {name}")
4547

4648
if cls._request_type is not None and cls._request_type != request_type:
47-
raise Exception(
49+
raise TypeError(
4850
f"Only one request type shall be recorded for class {name} "
4951
f"but found {cls._request_type} and {request_type}"
5052
)
5153
cls._request_type = request_type
54+
cls._synchronizer = dct.get("synchronizer")
55+
56+
if cls._synchronizer is None:
57+
raise TypeError(f"Request synchronizer not provided for class {name}")
5258

5359
return new_class
5460

5561
@classmethod
5662
def get_request_type(cls):
5763
return cls._request_type
5864

65+
@classmethod
66+
def get_synchronizer(cls):
67+
return cls._synchronizer
68+
5969
@classmethod
6070
def check_type(cls, pytype: type) -> bool:
6171
if pytype is not None and inspect.isclass(pytype):
@@ -65,6 +75,12 @@ def check_type(cls, pytype: type) -> bool:
6575
return False
6676

6777

78+
class RequestSynchronizer(abc.ABC):
79+
@abstractmethod
80+
def sync_route_params(self, request, path_params):
81+
raise NotImplementedError()
82+
83+
6884
class ResponseTrackerMeta(type):
6985
_response_types = {}
7086

@@ -75,14 +91,14 @@ def __new__(cls, name, bases, dct, **kwargs):
7591
response_type = dct.get("response_type")
7692

7793
if label is None:
78-
raise Exception(f"Response label not provided for class {name}")
94+
raise TypeError(f"Response label not provided for class {name}")
7995
if response_type is None:
80-
raise Exception(f"Response type not provided for class {name}")
96+
raise TypeError(f"Response type not provided for class {name}")
8197
if (
8298
cls._response_types.get(label) is not None
8399
and cls._response_types.get(label) != response_type
84100
):
85-
raise Exception(
101+
raise TypeError(
86102
f"Only one response type shall be recorded for class {name} "
87103
f"but found {cls._response_types.get(label)} and {response_type}"
88104
)
@@ -109,25 +125,29 @@ def check_type(cls, pytype: type) -> bool:
109125
return False
110126

111127

112-
class WebApp(metaclass=ModuleTrackerMeta):
128+
class ABCModuleTrackerMeta(abc.ABCMeta, ModuleTrackerMeta):
129+
pass
130+
131+
132+
class WebApp(metaclass=ABCModuleTrackerMeta):
113133
@abstractmethod
114134
def route(self, func: Callable):
115-
pass
135+
raise NotImplementedError()
116136

117137
@abstractmethod
118138
def get_app(self):
119-
pass
139+
raise NotImplementedError()
120140

121141

122-
class WebServer(metaclass=ModuleTrackerMeta):
142+
class WebServer(metaclass=ABCModuleTrackerMeta):
123143
def __init__(self, hostname, port, web_app: WebApp):
124144
self.hostname = hostname
125145
self.port = port
126146
self.web_app = web_app.get_app()
127147

128148
@abstractmethod
129149
async def serve(self):
130-
pass
150+
raise NotImplementedError() # pragma: no cover
131151

132152

133153
class HttpV2FeatureChecker:
@@ -146,3 +166,10 @@ class ResponseLabels(Enum):
146166
PLAIN_TEXT = "plain_text"
147167
REDIRECT = "redirect"
148168
UJSON = "ujson"
169+
INT = "int"
170+
FLOAT = "float"
171+
STR = "str"
172+
LIST = "list"
173+
DICT = "dict"
174+
BOOL = "bool"
175+
PYDANTIC = "pydantic"

azure-functions-extension-base/tests/test_web.py

Lines changed: 152 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import unittest
2-
from unittest.mock import patch
2+
from unittest.mock import MagicMock, patch
33

44
from azure.functions.extension.base import (
55
HttpV2FeatureChecker,
66
ModuleTrackerMeta,
7+
RequestSynchronizer,
78
RequestTrackerMeta,
89
ResponseLabels,
910
ResponseTrackerMeta,
@@ -66,6 +67,10 @@ class TestRequest2:
6667
class TestRequest3:
6768
pass
6869

70+
class Syncronizer(RequestSynchronizer):
71+
def sync_route_params(self, request, path_params):
72+
pass
73+
6974
def setUp(self):
7075
# Reset _request_type before each test
7176
RequestTrackerMeta._request_type = None
@@ -81,42 +86,71 @@ class TestClass(metaclass=RequestTrackerMeta):
8186
str(context.exception), "Request type not provided for class TestClass"
8287
)
8388

89+
def test_request_synchronizer_not_provided(self):
90+
# Define a class without providing the synchronizer attribute
91+
with self.assertRaises(Exception) as context:
92+
93+
class TestClass(metaclass=RequestTrackerMeta):
94+
request_type = self.TestRequest1
95+
96+
self.assertEqual(
97+
str(context.exception),
98+
"Request synchronizer not provided for class TestClass",
99+
)
100+
84101
def test_single_request_type(self):
85102
# Define a class providing a request_type attribute
86103
class TestClass(metaclass=RequestTrackerMeta):
87104
request_type = self.TestRequest1
105+
synchronizer = self.Syncronizer()
88106

89107
# Ensure the request_type is correctly recorded
90108
self.assertEqual(RequestTrackerMeta.get_request_type(), self.TestRequest1)
109+
self.assertTrue(
110+
isinstance(RequestTrackerMeta.get_synchronizer(), RequestSynchronizer)
111+
)
91112
# Ensure check_type returns True for the provided request_type
92113
self.assertTrue(RequestTrackerMeta.check_type(self.TestRequest1))
114+
self.assertFalse(RequestTrackerMeta.check_type(self.TestRequest2))
93115

94116
def test_multiple_request_types_same(self):
95117
# Define a class providing the same request_type attribute
96118
class TestClass1(metaclass=RequestTrackerMeta):
97119
request_type = self.TestRequest1
120+
synchronizer = self.Syncronizer()
98121

99122
# Ensure the request_type is correctly recorded
100123
self.assertEqual(RequestTrackerMeta.get_request_type(), self.TestRequest1)
124+
self.assertTrue(
125+
isinstance(RequestTrackerMeta.get_synchronizer(), RequestSynchronizer)
126+
)
101127
# Ensure check_type returns True for the provided request_type
102128
self.assertTrue(RequestTrackerMeta.check_type(self.TestRequest1))
103129

104130
# Define another class providing the same request_type attribute
105131
class TestClass2(metaclass=RequestTrackerMeta):
106132
request_type = self.TestRequest1
133+
synchronizer = self.Syncronizer()
107134

108135
# Ensure the request_type remains the same
109136
self.assertEqual(RequestTrackerMeta.get_request_type(), self.TestRequest1)
137+
self.assertTrue(
138+
isinstance(RequestTrackerMeta.get_synchronizer(), RequestSynchronizer)
139+
)
110140
# Ensure check_type still returns True for the original request_type
111141
self.assertTrue(RequestTrackerMeta.check_type(self.TestRequest1))
112142

113143
def test_multiple_request_types_different(self):
114144
# Define a class providing a different request_type attribute
115145
class TestClass1(metaclass=RequestTrackerMeta):
116146
request_type = self.TestRequest1
147+
synchronizer = self.Syncronizer()
117148

118149
# Ensure the request_type is correctly recorded
119150
self.assertEqual(RequestTrackerMeta.get_request_type(), self.TestRequest1)
151+
self.assertTrue(
152+
isinstance(RequestTrackerMeta.get_synchronizer(), RequestSynchronizer)
153+
)
120154
# Ensure check_type returns True for the provided request_type
121155
self.assertTrue(RequestTrackerMeta.check_type(self.TestRequest1))
122156

@@ -134,9 +168,30 @@ class TestClass2(metaclass=RequestTrackerMeta):
134168

135169
# Ensure the request_type remains the same after the exception
136170
self.assertEqual(RequestTrackerMeta.get_request_type(), self.TestRequest1)
171+
self.assertTrue(
172+
isinstance(RequestTrackerMeta.get_synchronizer(), RequestSynchronizer)
173+
)
137174
# Ensure check_type still returns True for the original request_type
138175
self.assertTrue(RequestTrackerMeta.check_type(self.TestRequest1))
139176

177+
def test_pytype_is_none(self):
178+
self.assertFalse(RequestTrackerMeta.check_type(None))
179+
180+
def test_pytype_is_not_class(self):
181+
self.assertFalse(RequestTrackerMeta.check_type("string"))
182+
183+
def test_sync_route_params_raises_not_implemented_error(self):
184+
class MockSyncronizer(RequestSynchronizer):
185+
def sync_route_params(self, request, path_params):
186+
super().sync_route_params(request, path_params)
187+
188+
# Create an instance of RequestSynchronizer
189+
synchronizer = MockSyncronizer()
190+
191+
# Ensure that calling sync_route_params raises NotImplementedError
192+
with self.assertRaises(NotImplementedError):
193+
synchronizer.sync_route_params(None, None)
194+
140195

141196
class TestResponseTrackerMeta(unittest.TestCase):
142197
class MockResponse1:
@@ -208,13 +263,36 @@ class TestResponse2(metaclass=ResponseTrackerMeta):
208263
ResponseTrackerMeta.get_response_type(ResponseLabels.STANDARD),
209264
self.MockResponse1,
210265
)
266+
self.assertEqual(
267+
ResponseTrackerMeta.get_standard_response_type(), self.MockResponse1
268+
)
211269
self.assertEqual(
212270
ResponseTrackerMeta.get_response_type(ResponseLabels.STREAMING),
213271
self.MockResponse2,
214272
)
215273
self.assertTrue(ResponseTrackerMeta.check_type(self.MockResponse1))
216274
self.assertTrue(ResponseTrackerMeta.check_type(self.MockResponse2))
217275

276+
def test_response_label_not_provided(self):
277+
with self.assertRaises(Exception) as context:
278+
279+
class TestResponse(metaclass=ResponseTrackerMeta):
280+
response_type = self.MockResponse1
281+
282+
self.assertEqual(
283+
str(context.exception), "Response label not provided for class TestResponse"
284+
)
285+
286+
def test_response_type_not_provided(self):
287+
with self.assertRaises(Exception) as context:
288+
289+
class TestResponse(metaclass=ResponseTrackerMeta):
290+
label = "test_label_1"
291+
292+
self.assertEqual(
293+
str(context.exception), "Response type not provided for class TestResponse"
294+
)
295+
218296

219297
class TestWebApp(unittest.TestCase):
220298
def test_route_and_get_app(self):
@@ -228,6 +306,34 @@ def get_app(self):
228306
app = MockWebApp()
229307
self.assertEqual(app.get_app(), "MockApp")
230308

309+
def test_route_method_raises_not_implemented_error(self):
310+
class MockWebApp(WebApp):
311+
def get_app(self):
312+
pass
313+
314+
def route(self, func):
315+
super().route(func)
316+
317+
with self.assertRaises(NotImplementedError):
318+
# Create a mock WebApp instance
319+
mock_web_app = MockWebApp()
320+
# Call the route method
321+
mock_web_app.route(None)
322+
323+
def test_get_app_method_raises_not_implemented_error(self):
324+
class MockWebApp(WebApp):
325+
def route(self, func):
326+
pass
327+
328+
def get_app(self):
329+
super().get_app()
330+
331+
with self.assertRaises(NotImplementedError):
332+
# Create a mock WebApp instance
333+
mock_web_app = MockWebApp()
334+
# Call the get_app method
335+
mock_web_app.get_app()
336+
231337

232338
class TestWebServer(unittest.TestCase):
233339
def test_web_server_initialization(self):
@@ -238,12 +344,36 @@ def route(self, func):
238344
def get_app(self):
239345
return "MockApp"
240346

347+
class MockWebServer(WebServer):
348+
async def serve(self):
349+
pass
350+
241351
mock_web_app = MockWebApp()
242-
server = WebServer("localhost", 8080, mock_web_app)
352+
server = MockWebServer("localhost", 8080, mock_web_app)
243353
self.assertEqual(server.hostname, "localhost")
244354
self.assertEqual(server.port, 8080)
245355
self.assertEqual(server.web_app, "MockApp")
246356

357+
async def test_serve_method_raises_not_implemented_error(self):
358+
# Create a mock WebApp instance
359+
class MockWebApp(WebApp):
360+
def route(self, func):
361+
pass
362+
363+
def get_app(self):
364+
pass
365+
366+
class MockWebServer(WebServer):
367+
async def serve(self):
368+
super().serve()
369+
370+
# Create a WebServer instance with the mock WebApp
371+
server = MockWebServer("localhost", 8080, MockWebApp())
372+
373+
# Ensure that calling the serve method raises NotImplementedError
374+
with self.assertRaises(NotImplementedError):
375+
await server.serve()
376+
247377

248378
class TestHttpV2Enabled(unittest.TestCase):
249379
@patch("azure.functions.extension.base.ModuleTrackerMeta.module_imported")
@@ -253,3 +383,23 @@ def test_http_v2_enabled(self, mock_module_imported):
253383

254384
mock_module_imported.return_value = False
255385
self.assertFalse(HttpV2FeatureChecker.http_v2_enabled())
386+
387+
388+
class TestResponseLabels(unittest.TestCase):
389+
def test_enum_values(self):
390+
self.assertEqual(ResponseLabels.STANDARD.value, "standard")
391+
self.assertEqual(ResponseLabels.STREAMING.value, "streaming")
392+
self.assertEqual(ResponseLabels.FILE.value, "file")
393+
self.assertEqual(ResponseLabels.HTML.value, "html")
394+
self.assertEqual(ResponseLabels.JSON.value, "json")
395+
self.assertEqual(ResponseLabels.ORJSON.value, "orjson")
396+
self.assertEqual(ResponseLabels.PLAIN_TEXT.value, "plain_text")
397+
self.assertEqual(ResponseLabels.REDIRECT.value, "redirect")
398+
self.assertEqual(ResponseLabels.UJSON.value, "ujson")
399+
self.assertEqual(ResponseLabels.INT.value, "int")
400+
self.assertEqual(ResponseLabels.FLOAT.value, "float")
401+
self.assertEqual(ResponseLabels.STR.value, "str")
402+
self.assertEqual(ResponseLabels.LIST.value, "list")
403+
self.assertEqual(ResponseLabels.DICT.value, "dict")
404+
self.assertEqual(ResponseLabels.BOOL.value, "bool")
405+
self.assertEqual(ResponseLabels.PYDANTIC.value, "pydantic")

0 commit comments

Comments
 (0)