Skip to content

Commit 9ff5c10

Browse files
authored
Merge pull request #814 from HardMax71/fix/strict-typing-other-handlers
fix(http): pass mypy --strict in the other middleware handlers and fix four handler bugs
2 parents 6ddcf12 + 6b43a82 commit 9ff5c10

8 files changed

Lines changed: 102 additions & 37 deletions

File tree

‎packages/http/httpx/kiota_http/middleware/headers_inspection_handler.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ def _get_current_options(self, request: httpx.Request) -> HeadersInspectionHandl
7070
Returns:
7171
HeadersInspectionHandlerOption: The options to be used.
7272
"""
73-
current_options = None
73+
current_options: Optional[HeadersInspectionHandlerOption] = None
7474
request_options = request.extensions.get(REQUEST_OPTIONS_KEY)
7575
if request_options:
7676
current_options = request_options.get(HeadersInspectionHandlerOption.get_key(), None)

‎packages/http/httpx/kiota_http/middleware/parameters_name_decoding_handler.py‎

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from typing import Optional
2+
13
from kiota_abstractions.request_option import RequestOption
24

35
import httpx
@@ -10,19 +12,16 @@
1012

1113
class ParametersNameDecodingHandler(BaseMiddleware):
1214

13-
def __init__(
14-
self,
15-
options: ParametersNameDecodingHandlerOption = ParametersNameDecodingHandlerOption(),
16-
):
15+
def __init__(self, options: Optional[ParametersNameDecodingHandlerOption] = None) -> None:
1716
"""Create an instance of ParametersNameDecodingHandler
1817
1918
Args:
2019
options (ParametersNameDecodingHandlerOption, optional): The parameters name
21-
decoding handler options value.
22-
Defaults to ParametersNameDecodingHandlerOption
20+
decoding handler options value. A new ParametersNameDecodingHandlerOption
21+
per handler when not provided.
2322
"""
2423
super().__init__()
25-
self.options = options
24+
self.options = options if options is not None else ParametersNameDecodingHandlerOption()
2625

2726
async def send(
2827
self, request: httpx.Request, transport: httpx.AsyncBaseTransport
@@ -72,7 +71,7 @@ def _get_current_options(self, request: httpx.Request) -> ParametersNameDecoding
7271
"""
7372
request_options = request.extensions.get(REQUEST_OPTIONS_KEY)
7473
if request_options:
75-
current_options = request_options.get(
74+
current_options: ParametersNameDecodingHandlerOption = request_options.get(
7675
ParametersNameDecodingHandlerOption.get_key(), self.options
7776
)
7877
return current_options

‎packages/http/httpx/kiota_http/middleware/redirect_handler.py‎

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,14 @@ class RedirectHandler(BaseMiddleware):
2727
STATUS_CODE_SEE_OTHER: int = 303
2828
LOCATION_HEADER: str = "Location"
2929

30-
def __init__(self, options: RedirectHandlerOption = RedirectHandlerOption()) -> None:
30+
def __init__(self, options: typing.Optional[RedirectHandlerOption] = None) -> None:
3131
super().__init__()
32-
self.options = options
32+
self.options = options if options is not None else RedirectHandlerOption()
3333
self.redirect_on_status_codes: set[int] = self.DEFAULT_REDIRECT_STATUS_CODES
3434

35-
def increment(self, response, max_redirect, history) -> bool:
35+
def increment(
36+
self, response: httpx.Response, max_redirect: int, history: list[httpx.Request]
37+
) -> bool:
3638
"""Increment the redirect attempts for this request.
3739
Args
3840
response(httpx.Response): A httpx response object.
@@ -53,7 +55,8 @@ def get_redirect_location(self, response: httpx.Response) -> typing.Union[str, b
5355
location. ``False`` if not a redirect status code.
5456
"""
5557
if response.status_code in self.redirect_on_status_codes:
56-
return response.headers.get('location')
58+
location: typing.Optional[str] = response.headers.get('location')
59+
return location
5760
return None
5861

5962
async def send(
@@ -114,8 +117,9 @@ def _get_current_options(self, request: httpx.Request) -> RedirectHandlerOption:
114117
"""
115118
request_options = request.extensions.get(REQUEST_OPTIONS_KEY)
116119
if request_options:
117-
current_options = request_options.get( # type:ignore
118-
RedirectHandlerOption.get_key(), self.options)
120+
current_options: RedirectHandlerOption = request_options.get(
121+
RedirectHandlerOption.get_key(), self.options
122+
)
119123
return current_options
120124
return self.options
121125

@@ -124,7 +128,7 @@ def _build_redirect_request(
124128
request: httpx.Request,
125129
response: httpx.Response,
126130
options: RedirectHandlerOption,
127-
request_options: typing.Optional[dict] = None,
131+
request_options: typing.Optional[dict[str, object]] = None,
128132
) -> httpx.Request:
129133
"""
130134
Given a request and a redirect response, return a new request that

‎packages/http/httpx/kiota_http/middleware/url_replace_handler.py‎

Lines changed: 19 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from typing import Optional
2+
13
from kiota_abstractions.request_option import RequestOption
24
from opentelemetry.semconv.attributes.url_attributes import URL_FULL
35

@@ -9,16 +11,16 @@
911

1012
class UrlReplaceHandler(BaseMiddleware):
1113

12-
def __init__(self, options: UrlReplaceHandlerOption = UrlReplaceHandlerOption()):
14+
def __init__(self, options: Optional[UrlReplaceHandlerOption] = None) -> None:
1315
"""Create an instance of UrlReplaceHandler
1416
1517
Args:
1618
options (UrlReplaceHandlerOption, optional): The url replacement
17-
options to pass to the handler.
18-
Defaults to UrlReplaceHandlerOption
19+
options to pass to the handler. A new UrlReplaceHandlerOption
20+
per handler when not provided.
1921
"""
2022
super().__init__()
21-
self.options = options
23+
self.options = options if options is not None else UrlReplaceHandlerOption()
2224

2325
async def send(
2426
self, request: httpx.Request, transport: httpx.AsyncBaseTransport
@@ -32,19 +34,19 @@ async def send(
3234
Returns:
3335
Response: The response object.
3436
"""
35-
response = None
3637
_enable_span = self._create_observability_span(request, "UrlReplaceHandler_send")
37-
if self.options and self.options.is_enabled:
38-
_enable_span.set_attribute("com.microsoft.kiota.handler.url_replacer.enable", True)
38+
try:
3939
current_options = self._get_current_options(request)
40+
if current_options.is_enabled:
41+
_enable_span.set_attribute("com.microsoft.kiota.handler.url_replacer.enable", True)
4042

41-
url_string: str = str(request.url)
42-
url_string = self.replace_url_segment(url_string, current_options)
43-
request.url = httpx.URL(url_string)
44-
_enable_span.set_attribute(URL_FULL, str(request.url))
45-
response = await super().send(request, transport)
46-
_enable_span.end()
47-
return response
43+
url_string: str = str(request.url)
44+
url_string = self.replace_url_segment(url_string, current_options)
45+
request.url = httpx.URL(url_string)
46+
_enable_span.set_attribute(URL_FULL, str(request.url))
47+
return await super().send(request, transport)
48+
finally:
49+
_enable_span.end()
4850

4951
def _get_current_options(self, request: httpx.Request) -> UrlReplaceHandlerOption:
5052
"""Returns the options to use for the request.Overries default options if
@@ -58,7 +60,9 @@ def _get_current_options(self, request: httpx.Request) -> UrlReplaceHandlerOptio
5860
"""
5961
request_options = request.extensions.get(REQUEST_OPTIONS_KEY)
6062
if request_options:
61-
current_options = request_options.get(UrlReplaceHandlerOption.get_key(), self.options)
63+
current_options: UrlReplaceHandlerOption = request_options.get(
64+
UrlReplaceHandlerOption.get_key(), self.options
65+
)
6266
return current_options
6367
return self.options
6468

‎packages/http/httpx/kiota_http/middleware/user_agent_handler.py‎

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from typing import Optional
2+
13
from kiota_abstractions.request_option import RequestOption
24

35
from httpx import AsyncBaseTransport, Request, Response
@@ -11,8 +13,8 @@ class UserAgentHandler(BaseMiddleware):
1113
Middleware handler for User Agent.
1214
"""
1315

14-
def __init__(self, options: UserAgentHandlerOption = UserAgentHandlerOption(), **kwargs):
15-
super().__init__(**kwargs)
16+
def __init__(self, options: Optional[UserAgentHandlerOption] = None) -> None:
17+
super().__init__()
1618
self.options = UserAgentHandlerOption() if options is None else options
1719

1820
async def send(self, request: Request, transport: AsyncBaseTransport) -> Response:
@@ -24,7 +26,7 @@ async def send(self, request: Request, transport: AsyncBaseTransport) -> Respons
2426
_span = self._create_observability_span(request, "UserAgentHandler_send")
2527
if current_options and current_options.is_enabled:
2628
_span.set_attribute("com.microsoft.kiota.handler.useragent.enable", True)
27-
value = f"{self.options.product_name}/{self.options.product_version}"
29+
value = f"{current_options.product_name}/{current_options.product_version}"
2830
self._update_user_agent(request, value)
2931
_span.end()
3032
return await super().send(request, transport)
@@ -41,11 +43,13 @@ def _get_current_options(self, request: Request) -> UserAgentHandlerOption:
4143
"""
4244
request_options = request.extensions.get(REQUEST_OPTIONS_KEY)
4345
if request_options:
44-
current_options = request_options.get(UserAgentHandlerOption.get_key(), self.options)
46+
current_options: UserAgentHandlerOption = request_options.get(
47+
UserAgentHandlerOption.get_key(), self.options
48+
)
4549
return current_options
4650
return self.options
4751

48-
def _update_user_agent(self, request: Request, value: str):
52+
def _update_user_agent(self, request: Request, value: str) -> None:
4953
"""Updates the values of the User-Agent header."""
5054
user_agent = request.headers.get("User-Agent", "")
5155
if not user_agent:

‎packages/http/httpx/tests/middleware_tests/test_url_replace_handler.py‎

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import httpx
22
import pytest
33

4-
from kiota_http.middleware import UrlReplaceHandler
4+
from kiota_http.middleware import REQUEST_OPTIONS_KEY, UrlReplaceHandler
55
from kiota_http.middleware.options import UrlReplaceHandlerOption
66

77
ORIGINAL_URL = "https://graph.microsoft.com/users/user-id-to-replace/messages"
@@ -53,3 +53,34 @@ def test_replace_url_segment():
5353
def test_default_replacement_pairs_are_not_shared():
5454
UrlReplaceHandlerOption().replacement_pairs["/users/user-id-to-replace"] = "/me"
5555
assert UrlReplaceHandlerOption().replacement_pairs == {}
56+
57+
58+
@pytest.mark.asyncio
59+
async def test_request_option_enables_replacement():
60+
option = UrlReplaceHandlerOption(
61+
enabled=True, replacement_pairs={"/users/user-id-to-replace": "/me"}
62+
)
63+
request = httpx.Request(
64+
"GET", ORIGINAL_URL, extensions={REQUEST_OPTIONS_KEY: {option.get_key(): option}}
65+
)
66+
handler = UrlReplaceHandler(options=UrlReplaceHandlerOption(enabled=False))
67+
68+
async with httpx.MockTransport(lambda request: httpx.Response(200)) as transport:
69+
response = await handler.send(request, transport)
70+
71+
assert str(response.request.url) == REPLACED_URL
72+
73+
74+
@pytest.mark.asyncio
75+
async def test_span_ends_on_transport_failure(span_exporter):
76+
77+
def request_handler(request):
78+
raise httpx.ReadError("request interrupted")
79+
80+
handler = UrlReplaceHandler()
81+
request = httpx.Request("GET", ORIGINAL_URL)
82+
async with httpx.MockTransport(request_handler) as transport:
83+
with pytest.raises(httpx.ReadError):
84+
await handler.send(request, transport)
85+
86+
assert [span.name for span in span_exporter.get_finished_spans()] == ["UrlReplaceHandler_send"]

‎packages/http/httpx/tests/middleware_tests/test_user_agent_handler.py‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1+
import httpx
12
import pytest
23

34
from kiota_http._version import VERSION
5+
from kiota_http.middleware import REQUEST_OPTIONS_KEY, UserAgentHandler
46
from kiota_http.middleware.options.user_agent_handler_option import UserAgentHandlerOption
57

68

@@ -27,3 +29,16 @@ def test_is_enabled_setter():
2729
options = UserAgentHandlerOption()
2830
options.is_enabled = False
2931
assert not options.is_enabled
32+
33+
34+
@pytest.mark.asyncio
35+
async def test_request_option_sets_the_product():
36+
option = UserAgentHandlerOption(product_name="custom", product_version="9")
37+
request = httpx.Request(
38+
"GET", "https://example.com", extensions={REQUEST_OPTIONS_KEY: {option.get_key(): option}}
39+
)
40+
41+
async with httpx.MockTransport(lambda request: httpx.Response(200)) as transport:
42+
response = await UserAgentHandler().send(request, transport)
43+
44+
assert response.request.headers["User-Agent"] == "custom/9"

‎packages/http/httpx/tests/test_kiota_client_factory.py‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,14 @@ def test_get_default_middleware():
141141
assert isinstance(middleware[6], BodyInspectionHandler)
142142

143143

144+
def test_default_middleware_does_not_share_options():
145+
first = KiotaClientFactory.get_default_middleware(None)
146+
second = KiotaClientFactory.get_default_middleware(None)
147+
148+
for first_handler, second_handler in zip(first, second):
149+
assert first_handler.options is not second_handler.options
150+
151+
144152
def test_get_default_middleware_with_options():
145153
"""Test fetching of default middleware with custom options passed"""
146154
retry_options = RetryHandlerOption(max_retries=7)

0 commit comments

Comments
 (0)