From f50ba3dda5ac36f157f7bda38791da8cb012776d Mon Sep 17 00:00:00 2001 From: Maksim Beliaev Date: Fri, 10 Feb 2023 12:10:16 +0100 Subject: [PATCH 1/3] parse matcher --- README.rst | 3 +-- responses/_recorder.py | 20 ++++++++++++++++++++ responses/matchers.py | 10 +++++----- responses/tests/test_recorder.py | 2 +- 4 files changed, 27 insertions(+), 8 deletions(-) diff --git a/README.rst b/README.rst index 6610e7e5..d2defc97 100644 --- a/README.rst +++ b/README.rst @@ -758,6 +758,7 @@ Example that shows how to set custom registry print("Before tests:", responses.mock.get_registry()) """ Before tests: """ + # using function decorator @responses.activate(registry=CustomRegistry) def run(): @@ -1024,7 +1025,6 @@ to check how many times each request was matched. @responses.activate def test_call_count_with_matcher(): - rsp = responses.get( "http://www.example.com", match=(matchers.query_param_matcher({}),), @@ -1308,7 +1308,6 @@ replaced. @responses.activate def test_replace(): - responses.get("http://example.org", json={"data": 1}) responses.replace(responses.GET, "http://example.org", json={"data": 2}) diff --git a/responses/_recorder.py b/responses/_recorder.py index 7142691f..9bec2fc7 100644 --- a/responses/_recorder.py +++ b/responses/_recorder.py @@ -1,6 +1,9 @@ +import inspect from functools import wraps from typing import TYPE_CHECKING +from responses.matchers import query_param_matcher + if TYPE_CHECKING: # pragma: no cover import os @@ -52,6 +55,11 @@ def _dump(registered: "List[BaseResponse]", destination: "BinaryIO") -> None: } } ) + 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." @@ -60,6 +68,17 @@ def _dump(registered: "List[BaseResponse]", destination: "BinaryIO") -> None: _toml_w.dump(_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, @@ -104,6 +123,7 @@ 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] ) 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 30134d70..275c473c 100644 --- a/responses/tests/test_recorder.py +++ b/responses/tests/test_recorder.py @@ -84,7 +84,7 @@ def test_recorder(self, httpserver): url400 = httpserver.url_for("/status/wrong") def another(): - requests.get(url500) + requests.get(url500, params={"query": "smth"}) requests.put(url202) @_recorder.record(file_path=self.out_file) From a708d3add0c6460d5b2dc3869d98044ed03ace8e Mon Sep 17 00:00:00 2001 From: Maksim Beliaev Date: Sun, 28 May 2023 15:12:11 +0200 Subject: [PATCH 2/3] added recorder tests added header matcher recording --- responses/_recorder.py | 6 +- responses/tests/test_recorder.py | 135 ++++++++++++++++++++++++++----- 2 files changed, 122 insertions(+), 19 deletions(-) diff --git a/responses/_recorder.py b/responses/_recorder.py index 736fa3dd..6c057242 100644 --- a/responses/_recorder.py +++ b/responses/_recorder.py @@ -2,6 +2,7 @@ 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 @@ -140,7 +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] + 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/tests/test_recorder.py b/responses/tests/test_recorder.py index d606b7ff..1d841e1e 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", } }, ] @@ -78,7 +177,7 @@ def test_recorder(self, httpserver): def another(): requests.get(url500, params={"query": "smth"}) - requests.put(url202) + requests.put(url202, headers={"XAuth": "54579"}) @_recorder.record(file_path=self.out_file) def run(): @@ -143,7 +242,7 @@ def prepare_server(self, httpserver): class TestReplay: def setup(self): - self.out_file = Path("response_record") + self.out_file = Path("response_record.yaml") def teardown(self): if self.out_file.exists(): From 3da417ea325e9f6de8a1e5f5c69587d15f7ad20f Mon Sep 17 00:00:00 2001 From: Maksim Beliaev Date: Sun, 28 May 2023 15:13:28 +0200 Subject: [PATCH 3/3] fix name --- responses/tests/test_recorder.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/responses/tests/test_recorder.py b/responses/tests/test_recorder.py index 1d841e1e..a5ad8f43 100644 --- a/responses/tests/test_recorder.py +++ b/responses/tests/test_recorder.py @@ -242,7 +242,7 @@ def prepare_server(self, httpserver): class TestReplay: def setup(self): - self.out_file = Path("response_record.yaml") + self.out_file = Path("response_record") def teardown(self): if self.out_file.exists():