diff --git a/responses/_recorder.py b/responses/_recorder.py index 93603ff7..6c057242 100644 --- a/responses/_recorder.py +++ b/responses/_recorder.py @@ -1,6 +1,10 @@ +import inspect from functools import wraps from typing import TYPE_CHECKING +from responses.matchers import header_matcher +from responses.matchers import query_param_matcher + if TYPE_CHECKING: # pragma: no cover import os @@ -58,6 +62,11 @@ def _dump( } } ) + if rsp.match: + matchers = data["responses"][-1]["response"]["matchers"] = [] + for matcher in rsp.match: + matchers.append(parse_matchers_function(matcher)) + except AttributeError as exc: # pragma: no cover raise AttributeError( "Cannot dump response object." @@ -66,6 +75,17 @@ def _dump( dumper(_remove_nones(data), destination) +def parse_matchers_function(func: "Callable[..., Any]") -> "Dict[str, Any]": + matcher_name = func.__qualname__.split(".")[0] + matcher_constructor: Dict[str, Any] = { + matcher_name: { + "args": inspect.getclosurevars(func).nonlocals, + "matcher_import_path": func.__module__, + } + } + return matcher_constructor + + class Recorder(RequestsMock): def __init__( self, @@ -121,6 +141,10 @@ def _on_request( url=str(requests_response.request.url), status=requests_response.status_code, body=requests_response.text, + match=( + query_param_matcher(request.params), # type: ignore[attr-defined] + header_matcher(dict(request.headers)), + ), ) self._registry.add(responses_response) return requests_response diff --git a/responses/matchers.py b/responses/matchers.py index 4880e508..73db64a4 100644 --- a/responses/matchers.py +++ b/responses/matchers.py @@ -207,13 +207,13 @@ def query_param_matcher( """ - params_dict = params or {} + def match(request: PreparedRequest) -> Tuple[bool, str]: + params_dict = params or {} - for k, v in params_dict.items(): - if isinstance(v, (int, float)): - params_dict[k] = str(v) + for k, v in params_dict.items(): + if isinstance(v, (int, float)): + params_dict[k] = str(v) - def match(request: PreparedRequest) -> Tuple[bool, str]: reason = "" request_params = request.params # type: ignore[attr-defined] request_params_dict = request_params or {} diff --git a/responses/tests/test_recorder.py b/responses/tests/test_recorder.py index 31402988..a5ad8f43 100644 --- a/responses/tests/test_recorder.py +++ b/responses/tests/test_recorder.py @@ -21,42 +21,141 @@ def get_data(host, port): "responses": [ { "response": { - "method": "GET", - "url": f"http://{host}:{port}/404", + "auto_calculate_content_length": False, "body": "404 Not Found", - "status": 404, "content_type": "text/plain", - "auto_calculate_content_length": False, + "matchers": [ + { + "query_param_matcher": { + "args": {"params": {}, "strict_match": True}, + "matcher_import_path": "responses.matchers", + } + }, + { + "header_matcher": { + "args": { + "headers": { + "Accept": "*/*", + "Accept-Encoding": "gzip, deflate", + "Connection": "keep-alive", + "User-Agent": "python-requests/2.30.0", + }, + "strict_match": False, + }, + "matcher_import_path": "responses.matchers", + } + }, + ], + "method": "GET", + "status": 404, + "url": f"http://{host}:{port}/404", } }, { "response": { - "method": "GET", - "url": f"http://{host}:{port}/status/wrong", + "auto_calculate_content_length": False, "body": "Invalid status code", - "status": 400, "content_type": "text/plain", - "auto_calculate_content_length": False, + "matchers": [ + { + "query_param_matcher": { + "args": {"params": {}, "strict_match": True}, + "matcher_import_path": "responses.matchers", + } + }, + { + "header_matcher": { + "args": { + "headers": { + "Accept": "*/*", + "Accept-Encoding": "gzip, deflate", + "Connection": "keep-alive", + "User-Agent": "python-requests/2.30.0", + }, + "strict_match": False, + }, + "matcher_import_path": "responses.matchers", + } + }, + ], + "method": "GET", + "status": 400, + "url": f"http://{host}:{port}/status/wrong", } }, { "response": { - "method": "GET", - "url": f"http://{host}:{port}/500", + "auto_calculate_content_length": False, "body": "500 Internal Server Error", - "status": 500, "content_type": "text/plain", - "auto_calculate_content_length": False, + "matchers": [ + { + "query_param_matcher": { + "args": { + "params": {"query": "smth"}, + "strict_match": True, + }, + "matcher_import_path": "responses.matchers", + } + }, + { + "header_matcher": { + "args": { + "headers": { + "Accept": "*/*", + "Accept-Encoding": "gzip, deflate", + "Connection": "keep-alive", + "User-Agent": "python-requests/2.30.0", + }, + "strict_match": False, + }, + "matcher_import_path": "responses.matchers", + } + }, + { + "query_string_matcher": { + "args": {"query": "query=smth"}, + "matcher_import_path": "responses.matchers", + } + }, + ], + "method": "GET", + "status": 500, + "url": f"http://{host}:{port}/500?query=smth", } }, { "response": { - "method": "PUT", - "url": f"http://{host}:{port}/202", + "auto_calculate_content_length": False, "body": "OK", - "status": 202, "content_type": "text/plain", - "auto_calculate_content_length": False, + "matchers": [ + { + "query_param_matcher": { + "args": {"params": {}, "strict_match": True}, + "matcher_import_path": "responses.matchers", + } + }, + { + "header_matcher": { + "args": { + "headers": { + "Accept": "*/*", + "Accept-Encoding": "gzip, deflate", + "Connection": "keep-alive", + "Content-Length": "0", + "User-Agent": "python-requests/2.30.0", + "XAuth": "54579", + }, + "strict_match": False, + }, + "matcher_import_path": "responses.matchers", + } + }, + ], + "method": "PUT", + "status": 202, + "url": f"http://{host}:{port}/202", } }, ] @@ -77,8 +176,8 @@ def test_recorder(self, httpserver): url202, url400, url404, url500 = self.prepare_server(httpserver) def another(): - requests.get(url500) - requests.put(url202) + requests.get(url500, params={"query": "smth"}) + requests.put(url202, headers={"XAuth": "54579"}) @_recorder.record(file_path=self.out_file) def run():