Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit a27d9d9

Browse files
authored
Add type hints to mocket.plugins.httpretty (#290)
* Add type hints to mocket.plugins.httpretty * Add types-requests test dependency * Add unit test to get_mocketize
1 parent 2ad3bec commit a27d9d9

File tree

6 files changed

+99
-30
lines changed

6 files changed

+99
-30
lines changed

mocket/mocket.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import itertools
55
import os
66
from pathlib import Path
7-
from typing import TYPE_CHECKING, ClassVar
7+
from typing import TYPE_CHECKING, Any, ClassVar
88

99
import mocket.inject
1010
from mocket.recording import MocketRecordStorage
@@ -99,12 +99,12 @@ def reset(cls) -> None:
9999
cls._record_storage = None
100100

101101
@classmethod
102-
def last_request(cls):
102+
def last_request(cls) -> Any:
103103
if cls.has_requests():
104104
return cls._requests[-1]
105105

106106
@classmethod
107-
def request_list(cls):
107+
def request_list(cls) -> list[Any]:
108108
return cls._requests
109109

110110
@classmethod

mocket/mocks/mockhttp.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ def __init__(self, body="", status=200, headers=None):
8888

8989
self.data = self.get_protocol_data() + self.body
9090

91-
def get_protocol_data(self, str_format_fun_name="capitalize"):
91+
def get_protocol_data(self, str_format_fun_name: str = "capitalize") -> bytes:
9292
status_line = f"HTTP/1.1 {self.status} {STATUS[self.status]}"
9393
header_lines = CRLF.join(
9494
(

mocket/plugins/httpretty/__init__.py

Lines changed: 26 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from typing import Any, Dict, Optional
2+
13
from mocket import mocketize
24
from mocket.async_mocket import async_mocketize
35
from mocket.compat import ENCODING
@@ -7,33 +9,35 @@
79
from mocket.mockhttp import Response as MocketHttpResponse
810

911

10-
def httprettifier_headers(headers):
12+
def httprettifier_headers(headers: Dict[str, str]) -> Dict[str, str]:
1113
return {k.lower().replace("_", "-"): v for k, v in headers.items()}
1214

1315

1416
class Request(MocketHttpRequest):
1517
@property
16-
def body(self):
17-
return super().body.encode(ENCODING)
18+
def body(self) -> bytes:
19+
return super().body.encode(ENCODING) # type: ignore[no-any-return]
1820

1921
@property
20-
def headers(self):
22+
def headers(self) -> Dict[str, str]:
2123
return httprettifier_headers(super().headers)
2224

2325

2426
class Response(MocketHttpResponse):
25-
def get_protocol_data(self, str_format_fun_name="lower"):
27+
headers: Dict[str, str]
28+
29+
def get_protocol_data(self, str_format_fun_name: str = "lower") -> bytes:
2630
if "server" in self.headers and self.headers["server"] == "Python/Mocket":
2731
self.headers["server"] = "Python/HTTPretty"
28-
return super().get_protocol_data(str_format_fun_name=str_format_fun_name)
32+
return super().get_protocol_data(str_format_fun_name=str_format_fun_name) # type: ignore[no-any-return]
2933

30-
def set_base_headers(self):
34+
def set_base_headers(self) -> None:
3135
super().set_base_headers()
3236
self.headers = httprettifier_headers(self.headers)
3337

3438
original_set_base_headers = set_base_headers
3539

36-
def set_extra_headers(self, headers):
40+
def set_extra_headers(self, headers: Dict[str, str]) -> None:
3741
self.headers.update(headers)
3842

3943

@@ -60,17 +64,17 @@ class Entry(MocketHttpEntry):
6064

6165

6266
def register_uri(
63-
method,
64-
uri,
65-
body="HTTPretty :)",
66-
adding_headers=None,
67-
forcing_headers=None,
68-
status=200,
69-
responses=None,
70-
match_querystring=False,
71-
priority=0,
72-
**headers,
73-
):
67+
method: str,
68+
uri: str,
69+
body: str = "HTTPretty :)",
70+
adding_headers: Optional[Dict[str, str]] = None,
71+
forcing_headers: Optional[Dict[str, str]] = None,
72+
status: int = 200,
73+
responses: Any = None,
74+
match_querystring: bool = False,
75+
priority: int = 0,
76+
**headers: str,
77+
) -> None:
7478
headers = httprettifier_headers(headers)
7579

7680
if adding_headers is not None:
@@ -81,9 +85,9 @@ def register_uri(
8185
def force_headers(self):
8286
self.headers = httprettifier_headers(forcing_headers)
8387

84-
Response.set_base_headers = force_headers
88+
Response.set_base_headers = force_headers # type: ignore[method-assign]
8589
else:
86-
Response.set_base_headers = Response.original_set_base_headers
90+
Response.set_base_headers = Response.original_set_base_headers # type: ignore[method-assign]
8791

8892
if responses:
8993
Entry.register(method, uri, *responses)
@@ -110,7 +114,7 @@ def __getattr__(self, name):
110114

111115

112116
HTTPretty = MocketHTTPretty()
113-
HTTPretty.register_uri = register_uri
117+
HTTPretty.register_uri = register_uri # type: ignore[attr-defined]
114118
httpretty = HTTPretty
115119

116120
__all__ = (

mocket/utils.py

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,34 @@
22

33
import binascii
44
import contextlib
5-
from typing import Callable
5+
from typing import Any, Callable, Protocol, TypeVar, overload
66

77
import decorator
8+
from typing_extensions import ParamSpec
89

910
from mocket.compat import decode_from_bytes, encode_to_bytes
1011

12+
_P = ParamSpec("_P")
13+
_R = TypeVar("_R")
14+
15+
16+
class MocketizeDecorator(Protocol):
17+
"""
18+
This is a generic decorator signature, currently applicable to get_mocketize.
19+
20+
Decorators can be used as:
21+
1. A function that transforms func (the parameter) into func1 (the returned object).
22+
2. A function that takes keyword arguments and returns 1.
23+
"""
24+
25+
@overload
26+
def __call__(self, func: Callable[_P, _R], /) -> Callable[_P, _R]: ...
27+
28+
@overload
29+
def __call__(
30+
self, **kwargs: Any
31+
) -> Callable[[Callable[_P, _R]], Callable[_P, _R]]: ...
32+
1133

1234
def hexdump(binary_string: bytes) -> str:
1335
r"""
@@ -30,11 +52,11 @@ def hexload(string: str) -> bytes:
3052
raise ValueError from e
3153

3254

33-
def get_mocketize(wrapper_: Callable) -> Callable:
55+
def get_mocketize(wrapper_: Callable) -> MocketizeDecorator:
3456
# trying to support different versions of `decorator`
3557
with contextlib.suppress(TypeError):
36-
return decorator.decorator(wrapper_, kwsyntax=True) # type: ignore[call-arg,unused-ignore]
37-
return decorator.decorator(wrapper_)
58+
return decorator.decorator(wrapper_, kwsyntax=True) # type: ignore[return-value, call-arg, unused-ignore]
59+
return decorator.decorator(wrapper_) # type: ignore[return-value]
3860

3961

4062
__all__ = (

pyproject.toml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ test = [
5757
"wait-for-it",
5858
"mypy",
5959
"types-decorator",
60+
"types-requests",
6061
]
6162
speedups = [
6263
"xxhash;platform_python_implementation=='CPython'",
@@ -123,6 +124,9 @@ files = [
123124
"mocket/exceptions.py",
124125
"mocket/compat.py",
125126
"mocket/utils.py",
127+
"mocket/plugins/httpretty/__init__.py",
128+
"tests/test_httpretty.py",
129+
"tests/test_mocket_utils.py",
126130
# "tests/"
127131
]
128132
strict = true
@@ -140,3 +144,11 @@ disable_error_code = ["no-untyped-def"] # enable this once full type-coverage is
140144
[[tool.mypy.overrides]]
141145
module = "tests.*"
142146
disable_error_code = ['type-arg', 'no-untyped-def']
147+
148+
[[tool.mypy.overrides]]
149+
module = "mocket.plugins.*"
150+
disallow_subclassing_any = false # mypy doesn't support dynamic imports
151+
152+
[[tool.mypy.overrides]]
153+
module = "tests.test_httpretty"
154+
disallow_untyped_decorators = true

tests/test_mocket_utils.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
from typing import Callable
2+
from unittest import TestCase
3+
from unittest.mock import NonCallableMock, patch
4+
5+
import decorator
6+
7+
from mocket.utils import get_mocketize
8+
9+
10+
def mock_decorator(func: Callable[[], None]) -> None:
11+
return func()
12+
13+
14+
class GetMocketizeTestCase(TestCase):
15+
@patch.object(decorator, "decorator")
16+
def test_get_mocketize_with_kwsyntax(self, dec: NonCallableMock) -> None:
17+
get_mocketize(mock_decorator)
18+
dec.assert_called_once_with(mock_decorator, kwsyntax=True)
19+
20+
@patch.object(decorator, "decorator")
21+
def test_get_mocketize_without_kwsyntax(self, dec: NonCallableMock) -> None:
22+
dec.side_effect = [
23+
TypeError("kwsyntax is not supported in this version of decorator"),
24+
mock_decorator,
25+
]
26+
27+
get_mocketize(mock_decorator)
28+
# First time called with kwsyntax=True, which failed with TypeError
29+
dec.call_args_list[0].assert_compare_to((mock_decorator,), {"kwsyntax": True})
30+
# Second time without kwsyntax, which succeeds
31+
dec.call_args_list[1].assert_compare_to((mock_decorator,))

0 commit comments

Comments
 (0)