Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package python-responses for openSUSE:Factory checked in at 2026-10-27 18:18:21 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/python-responses (Old) and /work/SRC/openSUSE:Factory/.python-responses.new.2252680 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-responses" Tue Oct 27 18:18:21 2026 rev:39 rq:1392582 version:0.26.3 Changes: -------- --- /work/SRC/openSUSE:Factory/python-responses/python-responses.changes 2026-07-07 21:00:08.455982845 +0200 +++ /work/SRC/openSUSE:Factory/.python-responses.new.2252680/python-responses.changes 2026-10-27 18:18:28.995398543 +0100 @@ -1,0 +2,20 @@ +Sun Oct 25 21:41:18 UTC 2026 - Dirk Müller <[email protected]> + +- update to 0.26.3: + * Added a ``keep_headers`` argument to ``_recorder.record`` and + ``Recorder.dump_to_file`` to preserve named headers (for + example ``Date``) that are otherwise stripped as verbose + defaults, so a signed response can be recorded and later + verified. Matching is case-insensitive. + * Fixed the element type exposed by `CallList` so static type + checkers infer `Call` values when iterating, indexing, or + filtering recorded calls. + * Fixed `query_string_matcher` (and the query matching auto- + applied to a registered URL's own query string) discarding + blank-valued query params (``b=``), which caused requests with + an extra or missing blank param to match incorrectly. See #804 + * Fixed `fragment_identifier_matcher` treating opaque fragments + (those without ``=``, e.g. ``/users/5``) as always equal, so a + required fragment matched a different one or none at all. See #806 + +------------------------------------------------------------------- Old: ---- responses-0.26.2.tar.gz New: ---- responses-0.26.3.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ python-responses.spec ++++++ --- /var/tmp/diff_new_pack.O1yUeN/_old 2026-10-27 18:18:32.790556826 +0100 +++ /var/tmp/diff_new_pack.O1yUeN/_new 2026-10-27 18:18:32.792556909 +0100 @@ -18,7 +18,7 @@ %{?sle15_python_module_pythons} Name: python-responses -Version: 0.26.2 +Version: 0.26.3 Release: 0 Summary: A utility library for mocking out the `requests` Python library License: Apache-2.0 ++++++ responses-0.26.2.tar.gz -> responses-0.26.3.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/responses-0.26.2/CHANGES new/responses-0.26.3/CHANGES --- old/responses-0.26.2/CHANGES 2026-07-03 18:42:35.000000000 +0200 +++ new/responses-0.26.3/CHANGES 2026-08-26 21:15:00.000000000 +0200 @@ -1,3 +1,20 @@ +0.26.3 +------ + +* Added a ``keep_headers`` argument to ``_recorder.record`` and + ``Recorder.dump_to_file`` to preserve named headers (for example ``Date``) + that are otherwise stripped as verbose defaults, so a signed response can be + recorded and later verified. Matching is case-insensitive. See #763 +* Fixed the element type exposed by `CallList` so static type checkers infer + `Call` values when iterating, indexing, or filtering recorded calls. See #722 +* Fixed `query_string_matcher` (and the query matching auto-applied to a + registered URL's own query string) discarding blank-valued query params + (``b=``), which caused requests with an extra or missing blank param to + match incorrectly. See #804 +* Fixed `fragment_identifier_matcher` treating opaque fragments (those without + ``=``, e.g. ``/users/5``) as always equal, so a required fragment matched a + different one or none at all. See #806 + 0.26.2 ------ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/responses-0.26.2/PKG-INFO new/responses-0.26.3/PKG-INFO --- old/responses-0.26.2/PKG-INFO 2026-07-03 18:42:43.970410800 +0200 +++ new/responses-0.26.3/PKG-INFO 2026-08-26 21:15:08.727542200 +0200 @@ -1,6 +1,6 @@ Metadata-Version: 2.4 Name: responses -Version: 0.26.2 +Version: 0.26.3 Summary: A utility library for mocking out the `requests` Python library. Home-page: https://github.com/getsentry/responses Author: David Cramer @@ -1455,6 +1455,17 @@ status: 202 url: https://httpstat.us/202 +Common headers such as ``Content-Type``, ``Date`` and ``Server`` are stripped +from the recording to keep the file terse. If you need to keep one of them, for +example a ``Date`` value that is part of a signed response you later verify, +pass its name in ``keep_headers`` (matched case-insensitively): + +.. code-block:: python + + @_recorder.record(file_path="out.yaml", keep_headers=["Date"]) + def test_recorder(): + ... + If you are in the REPL, you can also activate the recorder for all following responses: .. code-block:: python diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/responses-0.26.2/README.rst new/responses-0.26.3/README.rst --- old/responses-0.26.2/README.rst 2026-07-03 18:42:35.000000000 +0200 +++ new/responses-0.26.3/README.rst 2026-08-26 21:15:00.000000000 +0200 @@ -1401,6 +1401,17 @@ status: 202 url: https://httpstat.us/202 +Common headers such as ``Content-Type``, ``Date`` and ``Server`` are stripped +from the recording to keep the file terse. If you need to keep one of them, for +example a ``Date`` value that is part of a signed response you later verify, +pass its name in ``keep_headers`` (matched case-insensitively): + +.. code-block:: python + + @_recorder.record(file_path="out.yaml", keep_headers=["Date"]) + def test_recorder(): + ... + If you are in the REPL, you can also activate the recorder for all following responses: .. code-block:: python diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/responses-0.26.2/responses/__init__.py new/responses-0.26.3/responses/__init__.py --- old/responses-0.26.2/responses/__init__.py 2026-07-03 18:42:35.000000000 +0200 +++ new/responses-0.26.3/responses/__init__.py 2026-08-26 21:15:00.000000000 +0200 @@ -234,7 +234,7 @@ return wrapper -class CallList(Sequence[Any], Sized): +class CallList(Sequence[Call], Sized): def __init__(self) -> None: self._calls: List[Call] = [] diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/responses-0.26.2/responses/_recorder.py new/responses-0.26.3/responses/_recorder.py --- old/responses-0.26.2/responses/_recorder.py 2026-07-03 18:42:35.000000000 +0200 +++ new/responses-0.26.3/responses/_recorder.py 2026-08-26 21:15:00.000000000 +0200 @@ -8,6 +8,7 @@ from typing import BinaryIO from typing import Callable from typing import Dict + from typing import Iterable from typing import List from typing import Optional from typing import Type @@ -38,11 +39,22 @@ return d -def _remove_default_headers(data: "Any") -> "Any": +def _remove_default_headers( + data: "Any", keep_headers: "Optional[Iterable[str]]" = None +) -> "Any": """ It would be too verbose to store these headers in the file generated by the record functionality. + + Header names passed in ``keep_headers`` are excluded from the default + stripping, so a caller can preserve an otherwise-removed header (for + example a ``Date`` value that is part of a signed response). Matching is + case-insensitive, consistent with the rest of this function. """ + # A bare string is iterable over its characters, which would silently keep + # nothing useful; treat it as a single header name. + if isinstance(keep_headers, str): + keep_headers = [keep_headers] if isinstance(data, dict): keys_to_remove = [ "Content-Length", @@ -54,7 +66,8 @@ ] # HTTP header names are case-insensitive, and HTTP/2 servers send them # lowercase, so match without regard to case. - keys_to_remove_lower = {key.lower() for key in keys_to_remove} + keep_lower = {key.lower() for key in keep_headers or ()} + keys_to_remove_lower = {key.lower() for key in keys_to_remove} - keep_lower for i, response in enumerate(data["responses"]): headers = data["responses"][i]["response"]["headers"] for key in list(headers): @@ -69,6 +82,7 @@ registered: "List[BaseResponse]", destination: "Union[BinaryIO, TextIOWrapper]", dumper: "Callable[[Union[Dict[Any, Any], List[Any]], Union[BinaryIO, TextIOWrapper]], Any]", + keep_headers: "Optional[Iterable[str]]" = None, ) -> None: data: Dict[str, Any] = {"responses": []} for rsp in registered: @@ -93,7 +107,10 @@ "Probably you use custom Response object that is missing required attributes" ) from exc - dumper(_remove_default_headers(_remove_nones(data)), destination) + dumper( + _remove_default_headers(_remove_nones(data), keep_headers=keep_headers), + destination, + ) class Recorder(RequestsMock): @@ -109,7 +126,10 @@ self._registry = OrderedRegistry() def record( - self, *, file_path: "Union[str, bytes, os.PathLike[Any]]" = "response.yaml" + self, + *, + file_path: "Union[str, bytes, os.PathLike[Any]]" = "response.yaml", + keep_headers: "Optional[Iterable[str]]" = None, ) -> "Union[Callable[[_F], _F], _F]": def deco_record(function: "_F") -> "Callable[..., Any]": @wraps(function) @@ -117,7 +137,9 @@ with self: ret = function(*args, **kwargs) self.dump_to_file( - file_path=file_path, registered=self.get_registry().registered + file_path=file_path, + registered=self.get_registry().registered, + keep_headers=keep_headers, ) return ret @@ -131,12 +153,19 @@ file_path: "Union[str, bytes, os.PathLike[Any]]", *, registered: "Optional[List[BaseResponse]]" = None, + keep_headers: "Optional[Iterable[str]]" = None, ) -> None: - """Dump the recorded responses to a file.""" + """Dump the recorded responses to a file. + + Header names listed in ``keep_headers`` are preserved in the output + even though they are normally stripped as verbose defaults (for + example ``Date`` when it is part of a signed response). Matching is + case-insensitive. + """ if registered is None: registered = self.get_registry().registered with open(file_path, "w") as file: - _dump(registered, file, yaml.dump) + _dump(registered, file, yaml.dump, keep_headers=keep_headers) def _on_request( self, diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/responses-0.26.2/responses/matchers.py new/responses-0.26.3/responses/matchers.py --- old/responses-0.26.2/responses/matchers.py 2026-07-03 18:42:35.000000000 +0200 +++ new/responses-0.26.3/responses/matchers.py 2026-08-26 21:15:00.000000000 +0200 @@ -176,9 +176,16 @@ reason = "" url_fragment = urlparse(request.url).fragment if identifier: - url_fragment_qsl = sorted(parse_qsl(url_fragment)) # type: ignore[type-var] - identifier_qsl = sorted(parse_qsl(identifier)) - valid = identifier_qsl == url_fragment_qsl + if "=" in identifier: + # Query-string-style fragment: compare order-insensitively. + url_fragment_qsl = sorted(parse_qsl(url_fragment)) # type: ignore[type-var] + identifier_qsl = sorted(parse_qsl(identifier)) + valid = identifier_qsl == url_fragment_qsl + else: + # Opaque fragment (e.g. "/users/5"): parse_qsl() yields no + # pairs, so any two opaque fragments would compare equal. + # Compare them verbatim instead. + valid = identifier == url_fragment else: valid = not url_fragment @@ -259,8 +266,12 @@ data = parse_url(request.url or "") request_query = data.query - request_qsl = sorted(parse_qsl(request_query)) if request_query else {} - matcher_qsl = sorted(parse_qsl(query)) if query else {} + request_qsl = ( + sorted(parse_qsl(request_query, keep_blank_values=True)) + if request_query + else {} + ) + matcher_qsl = sorted(parse_qsl(query, keep_blank_values=True)) if query else {} valid = not query if request_query is None else request_qsl == matcher_qsl diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/responses-0.26.2/responses/tests/test_matchers.py new/responses-0.26.3/responses/tests/test_matchers.py --- old/responses-0.26.2/responses/tests/test_matchers.py 2026-07-03 18:42:35.000000000 +0200 +++ new/responses-0.26.3/responses/tests/test_matchers.py 2026-08-26 21:15:00.000000000 +0200 @@ -69,6 +69,40 @@ assert_reset() +def test_query_string_matcher_blank_values(): + """A blank-valued query param (``foo=``) is significant: it must be present + to match, and an extra blank param must not be ignored.""" + + def run(): + with responses.RequestsMock(assert_all_requests_are_fired=False) as rsps: + rsps.add( + responses.GET, + "http://example.com", + body=b"test", + match=[matchers.query_string_matcher("test=1&foo=")], + ) + # Exact match, including the blank param, succeeds. + resp = requests.get("http://example.com?test=1&foo=") + assert_response(resp, "test") + # Omitting the required blank param must not match. + with pytest.raises(ConnectionError): + requests.get("http://example.com?test=1") + + with responses.RequestsMock(assert_all_requests_are_fired=False) as rsps: + rsps.add( + responses.GET, + "http://example.com", + body=b"test", + match=[matchers.query_string_matcher("test=1")], + ) + # An extra blank param must not be silently ignored. + with pytest.raises(ConnectionError): + requests.get("http://example.com?test=1&foo=") + + run() + assert_reset() + + def test_request_matches_post_params(): @responses.activate def run(deprecated): @@ -937,6 +971,37 @@ assert_response(resp, "test") run() + assert_reset() + + +def test_fragment_identifier_matcher_opaque(): + """Opaque (non key=value) fragments must compare verbatim. + + Previously ``parse_qsl`` reduced any opaque fragment to ``[]``, so a + required ``#/users/5`` matched any other opaque fragment (or none). + """ + + @responses.activate + def run(): + responses.add( + responses.GET, + "http://example.com/", + match=[matchers.fragment_identifier_matcher("/users/5")], + body=b"test", + ) + + # A different opaque fragment, or no fragment, must not match. + with pytest.raises(ConnectionError) as excinfo: + requests.get("http://example.com/#/users/6") + assert ( + "URL fragment identifier is different: /users/5 doesn't match /users/6" + ) in str(excinfo.value) + + # The exact opaque fragment matches. + resp = requests.get("http://example.com/#/users/5") + assert_response(resp, "test") + + run() assert_reset() diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/responses-0.26.2/responses/tests/test_recorder.py new/responses-0.26.3/responses/tests/test_recorder.py --- old/responses-0.26.2/responses/tests/test_recorder.py 2026-07-03 18:42:35.000000000 +0200 +++ new/responses-0.26.3/responses/tests/test_recorder.py 2026-08-26 21:15:00.000000000 +0200 @@ -106,6 +106,63 @@ assert "headers" not in result["responses"][1]["response"] +def test_remove_default_headers_keeps_requested_headers(): + """Headers named in ``keep_headers`` survive the default-header stripping. + + Some consumers need a normally-stripped header in the recorded file, for + example a ``Date`` value that is part of a signed response they later + verify. Matching is case-insensitive so a lowercase HTTP/2 name is kept + too, while the other default headers are still removed. + """ + data = { + "responses": [ + { + "response": { + "headers": { + "date": "Mon, 01 Jan 2024 00:00:00 GMT", + "Server": "nginx", + "Content-Length": "12", + "x-custom": "keep-me", + } + } + }, + ] + } + + result = _remove_default_headers(data, keep_headers=["Date"]) + + assert result["responses"][0]["response"]["headers"] == { + "date": "Mon, 01 Jan 2024 00:00:00 GMT", + "x-custom": "keep-me", + } + + +def test_remove_default_headers_accepts_a_single_string(): + """A bare string is treated as one header name, not a set of characters.""" + data = { + "responses": [ + {"response": {"headers": {"Date": "d", "Server": "nginx"}}}, + ] + } + + result = _remove_default_headers(data, keep_headers="Date") + + assert result["responses"][0]["response"]["headers"] == {"Date": "d"} + + +def test_remove_default_headers_keep_non_default_is_noop(): + """Naming a non-default header does not change which headers are stripped.""" + data = { + "responses": [ + {"response": {"headers": {"Date": "d", "x-custom": "keep-me"}}}, + ] + } + + result = _remove_default_headers(data, keep_headers=["x-custom"]) + + assert result["responses"][0]["response"]["headers"] == {"x-custom": "keep-me"} + + class TestRecord: def setup_method(self): self.out_file = Path("response_record") @@ -133,12 +190,57 @@ data = yaml.safe_load(file) assert data == get_data(httpserver.host, httpserver.port) + def test_recorder_keep_headers_preserves_date(self, httpserver): + """``keep_headers`` keeps a normally-stripped header in the record. + + Reproduces the reported need to record the ``Date`` header so a signed + response can be verified against the recording (issue #763). Without + ``keep_headers`` the same header is stripped as a verbose default. + """ + httpserver.expect_request("/signed").respond_with_data( + "ok", + status=200, + content_type="text/plain", + headers={"Date": "Mon, 01 Jan 2024 00:00:00 GMT"}, + ) + url = httpserver.url_for("/signed") + + @_recorder.record(file_path=self.out_file, keep_headers=["Date"]) + def run_kept(): + requests.get(url) + + run_kept() + with open(self.out_file) as file: + kept = yaml.safe_load(file) + kept_headers = kept["responses"][0]["response"]["headers"] + # The server owns the exact Date value, so assert the header survives + # rather than pinning its contents. + assert "Date" in kept_headers + assert "2024" in kept_headers["Date"] + + self.out_file.unlink() + + @_recorder.record(file_path=self.out_file) + def run_default(): + requests.get(url) + + run_default() + with open(self.out_file) as file: + default = yaml.safe_load(file) + default_headers = default["responses"][0]["response"].get("headers", {}) + assert "Date" not in default_headers + def test_recorder_toml(self, httpserver): custom_recorder = _recorder.Recorder() - def dump_to_file(file_path, registered): + def dump_to_file(file_path, registered, *, keep_headers=None): with open(file_path, "wb") as file: - _dump(registered, file, tomli_w.dump) # type: ignore[arg-type] + _dump( + registered, + file, + tomli_w.dump, # type: ignore[arg-type] + keep_headers=keep_headers, + ) custom_recorder.dump_to_file = dump_to_file # type: ignore[assignment] diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/responses-0.26.2/responses.egg-info/PKG-INFO new/responses-0.26.3/responses.egg-info/PKG-INFO --- old/responses-0.26.2/responses.egg-info/PKG-INFO 2026-07-03 18:42:43.000000000 +0200 +++ new/responses-0.26.3/responses.egg-info/PKG-INFO 2026-08-26 21:15:08.000000000 +0200 @@ -1,6 +1,6 @@ Metadata-Version: 2.4 Name: responses -Version: 0.26.2 +Version: 0.26.3 Summary: A utility library for mocking out the `requests` Python library. Home-page: https://github.com/getsentry/responses Author: David Cramer @@ -1455,6 +1455,17 @@ status: 202 url: https://httpstat.us/202 +Common headers such as ``Content-Type``, ``Date`` and ``Server`` are stripped +from the recording to keep the file terse. If you need to keep one of them, for +example a ``Date`` value that is part of a signed response you later verify, +pass its name in ``keep_headers`` (matched case-insensitively): + +.. code-block:: python + + @_recorder.record(file_path="out.yaml", keep_headers=["Date"]) + def test_recorder(): + ... + If you are in the REPL, you can also activate the recorder for all following responses: .. code-block:: python diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/responses-0.26.2/setup.py new/responses-0.26.3/setup.py --- old/responses-0.26.2/setup.py 2026-07-03 18:42:35.000000000 +0200 +++ new/responses-0.26.3/setup.py 2026-08-26 21:15:00.000000000 +0200 @@ -36,7 +36,7 @@ setup( name="responses", - version="0.26.2", + version="0.26.3", author="David Cramer", description="A utility library for mocking out the `requests` Python library.", url="https://github.com/getsentry/responses",
