Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added real_adapter_send parameter to RequestsMock #671

Merged
merged 5 commits into from
Oct 4, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGES
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
0.24.0
------

* Added `real_adapter_send` parameter to `RequestsMock` that will allow users to set
through which function they would like to send real requests
* Added support for re.Pattern based header matching.
* Added support for gzipped response bodies to `json_params_matcher`.
* Moved types-pyyaml dependency to `tests_requires`
Expand Down
89 changes: 52 additions & 37 deletions responses/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,14 +84,25 @@ def __call__(
) -> models.Response:
...


# Block of type annotations
_Body = Union[str, BaseException, "Response", BufferedReader, bytes, None]
_F = Callable[..., Any]
_HeaderSet = Optional[Union[Mapping[str, str], List[Tuple[str, str]]]]
_MatcherIterable = Iterable[Callable[..., Tuple[bool, str]]]
_HTTPMethodOrResponse = Optional[Union[str, "BaseResponse"]]
_URLPatternType = Union["Pattern[str]", str]
# Block of type annotations
_Body = Union[str, BaseException, "Response", BufferedReader, bytes, None]
_F = Callable[..., Any]
_HeaderSet = Optional[Union[Mapping[str, str], List[Tuple[str, str]]]]
_MatcherIterable = Iterable[Callable[..., Tuple[bool, str]]]
_HTTPMethodOrResponse = Optional[Union[str, "BaseResponse"]]
_URLPatternType = Union["Pattern[str]", str]
_HTTPAdapterSend = Callable[
[
HTTPAdapter,
PreparedRequest,
bool,
float | tuple[float, float] | tuple[float, None] | None,
bool | str,
bytes | str | tuple[bytes | str, bytes | str] | None,
Mapping[str, str] | None,
],
models.Response,
]


Call = namedtuple("Call", ["request", "response"])
Expand Down Expand Up @@ -250,16 +261,16 @@ def __getitem__(self, idx: slice) -> List[Call]:
def __getitem__(self, idx: Union[int, slice]) -> Union[Call, List[Call]]:
return self._calls[idx]

def add(self, request: "PreparedRequest", response: _Body) -> None:
def add(self, request: "PreparedRequest", response: "_Body") -> None:
self._calls.append(Call(request, response))

def reset(self) -> None:
self._calls = []


def _ensure_url_default_path(
url: _URLPatternType,
) -> _URLPatternType:
url: "_URLPatternType",
) -> "_URLPatternType":
"""Add empty URL path '/' if doesn't exist.

Examples
Expand Down Expand Up @@ -376,15 +387,15 @@ class BaseResponse:
def __init__(
self,
method: str,
url: _URLPatternType,
url: "_URLPatternType",
match_querystring: Union[bool, object] = None,
match: "_MatcherIterable" = (),
*,
passthrough: bool = False,
) -> None:
self.method: str = method
# ensure the url has a default path set if the url is a string
self.url: _URLPatternType = _ensure_url_default_path(url)
self.url: "_URLPatternType" = _ensure_url_default_path(url)

if self._should_match_querystring(match_querystring):
match = tuple(match) + (
Expand Down Expand Up @@ -434,7 +445,7 @@ def _should_match_querystring(

return bool(urlsplit(self.url).query)

def _url_matches(self, url: _URLPatternType, other: str) -> bool:
def _url_matches(self, url: "_URLPatternType", other: str) -> bool:
"""Compares two URLs.

Compares only scheme, netloc and path. If 'url' is a re.Pattern, then checks that
Expand Down Expand Up @@ -532,8 +543,8 @@ class Response(BaseResponse):
def __init__(
self,
method: str,
url: _URLPatternType,
body: _Body = "",
url: "_URLPatternType",
body: "_Body" = "",
json: Optional[Any] = None,
status: int = 200,
headers: Optional[Mapping[str, str]] = None,
Expand All @@ -556,7 +567,7 @@ def __init__(
else:
content_type = "text/plain"

self.body: _Body = body
self.body: "_Body" = body
self.status: int = status
self.headers: Optional[Mapping[str, str]] = headers

Expand Down Expand Up @@ -608,7 +619,7 @@ class CallbackResponse(BaseResponse):
def __init__(
self,
method: str,
url: _URLPatternType,
url: "_URLPatternType",
callback: Callable[[Any], Any],
stream: Optional[bool] = None,
content_type: Optional[str] = "text/plain",
Expand Down Expand Up @@ -678,6 +689,8 @@ def __init__(
passthru_prefixes: Tuple[str, ...] = (),
target: str = "requests.adapters.HTTPAdapter.send",
registry: Type[FirstMatchRegistry] = FirstMatchRegistry,
*,
real_adapter_send: "_HTTPAdapterSend" = _real_send,
) -> None:
self._calls: CallList = CallList()
self.reset()
Expand All @@ -688,6 +701,7 @@ def __init__(
self.target: str = target
self._patcher: Optional["_mock_patcher[Any]"] = None
self._thread_lock = _ThreadingLock()
self._real_send = real_adapter_send

def get_registry(self) -> FirstMatchRegistry:
"""Returns current registry instance with responses.
Expand Down Expand Up @@ -726,10 +740,10 @@ def reset(self) -> None:

def add(
self,
method: _HTTPMethodOrResponse = None,
method: "_HTTPMethodOrResponse" = None,
url: "Optional[_URLPatternType]" = None,
body: _Body = "",
adding_headers: _HeaderSet = None,
body: "_Body" = "",
adding_headers: "_HeaderSet" = None,
*args: Any,
**kwargs: Any,
) -> BaseResponse:
Expand Down Expand Up @@ -808,7 +822,7 @@ def _add_from_file(self, file_path: "Union[str, bytes, os.PathLike[Any]]") -> No
auto_calculate_content_length=rsp["auto_calculate_content_length"],
)

def add_passthru(self, prefix: _URLPatternType) -> None:
def add_passthru(self, prefix: "_URLPatternType") -> None:
"""
Register a URL prefix or regex to passthru any non-matching mock requests to.

Expand All @@ -829,7 +843,7 @@ def add_passthru(self, prefix: _URLPatternType) -> None:

def remove(
self,
method_or_response: _HTTPMethodOrResponse = None,
method_or_response: "_HTTPMethodOrResponse" = None,
url: "Optional[_URLPatternType]" = None,
) -> List[BaseResponse]:
"""
Expand All @@ -852,9 +866,9 @@ def remove(

def replace(
self,
method_or_response: _HTTPMethodOrResponse = None,
method_or_response: "_HTTPMethodOrResponse" = None,
url: "Optional[_URLPatternType]" = None,
body: _Body = "",
body: "_Body" = "",
*args: Any,
**kwargs: Any,
) -> BaseResponse:
Expand All @@ -878,9 +892,9 @@ def replace(

def upsert(
self,
method_or_response: _HTTPMethodOrResponse = None,
method_or_response: "_HTTPMethodOrResponse" = None,
url: "Optional[_URLPatternType]" = None,
body: _Body = "",
body: "_Body" = "",
*args: Any,
**kwargs: Any,
) -> BaseResponse:
Expand All @@ -901,9 +915,10 @@ def upsert(
def add_callback(
self,
method: str,
url: _URLPatternType,
url: "_URLPatternType",
callback: Callable[
["PreparedRequest"], Union[Exception, Tuple[int, Mapping[str, str], _Body]]
["PreparedRequest"],
Union[Exception, Tuple[int, Mapping[str, str], "_Body"]],
],
match_querystring: Union[bool, FalseBool] = FalseBool(),
content_type: Optional[str] = "text/plain",
Expand Down Expand Up @@ -940,7 +955,7 @@ def __exit__(self, type: Any, value: Any, traceback: Any) -> bool:
return success

@overload
def activate(self, func: _F = ...) -> _F:
def activate(self, func: "_F" = ...) -> "_F":
"""Overload for scenario when 'responses.activate' is used."""

@overload
Expand All @@ -958,15 +973,15 @@ def activate(

def activate(
self,
func: Optional[_F] = None,
func: Optional["_F"] = None,
*,
registry: Optional[Type[Any]] = None,
assert_all_requests_are_fired: bool = False,
) -> Union[Callable[["_F"], "_F"], _F]:
) -> Union[Callable[["_F"], "_F"], "_F"]:
if func is not None:
return get_wrapped(func, self)

def deco_activate(function: _F) -> Callable[..., Any]:
def deco_activate(function: "_F") -> Callable[..., Any]:
return get_wrapped(
function,
self,
Expand Down Expand Up @@ -1008,7 +1023,7 @@ def _on_request(
*,
retries: Optional["_Retry"] = None,
**kwargs: Any,
) -> "models.Response":
) -> "Union[models.Response, models.Response]":
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why does this union have two of the same thing?

# add attributes params and req_kwargs to 'request' object for further match comparison
# original request object does not have these attributes
request.params = self._parse_request_params(request.path_url) # type: ignore[attr-defined]
Expand All @@ -1028,7 +1043,7 @@ def _on_request(
]
):
logger.info("request.allowed-passthru", extra={"url": request_url})
return _real_send(adapter, request, **kwargs)
return self._real_send(adapter, request, **kwargs) # type: ignore

error_msg = (
"Connection refused by Responses - the call doesn't "
Expand All @@ -1048,14 +1063,14 @@ def _on_request(
error_msg += f"- {p}\n"

response = ConnectionError(error_msg)
response.request = request
response.request = request # type: ignore[assignment]

self._calls.add(request, response)
raise response

if match.passthrough:
logger.info("request.passthrough-response", extra={"url": request_url})
response = _real_send(adapter, request, **kwargs) # type: ignore[assignment]
response = self._real_send(adapter, request, **kwargs) # type: ignore
else:
try:
response = adapter.build_response( # type: ignore[no-untyped-call]
Expand Down
28 changes: 28 additions & 0 deletions responses/tests/test_responses.py
Original file line number Diff line number Diff line change
Expand Up @@ -1838,6 +1838,34 @@ def run():
run()
assert_reset()

def test_real_send_argument(self):
def run():
# the following mock will serve to catch the real send request from another mock and
# will "donate" `unbound_on_send` method
mock_to_catch_real_send = responses.RequestsMock(
assert_all_requests_are_fired=True
)
mock_to_catch_real_send.post(
re.compile(r"http://localhost:7700.*"), status=500
)

with responses.RequestsMock(
assert_all_requests_are_fired=True,
real_adapter_send=mock_to_catch_real_send.unbound_on_send(),
) as r_mock:
r_mock.add_passthru(re.compile(r"http://localhost:7700.*"))

r_mock.add(responses.POST, "https://example.org", status=200)

response = requests.post("https://example.org")
assert response.status_code == 200

response = requests.post("http://localhost:7700/indexes/test/documents")
assert response.status_code == 500
beliaev-maksim marked this conversation as resolved.
Show resolved Hide resolved

run()
assert_reset()


def test_method_named_param():
@responses.activate
Expand Down