Repository: getsentry/responses
Pull request #489 (2022-01-31 15:22:55)

minor cleanup in imports in tests and f-strings

codecov[bot]: # [Codecov](https://app.codecov.io/gh/getsentry/responses/compare/489?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getsentry) Report
> Merging [#489](https://app.codecov.io/gh/getsentry/responses/compare/489?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getsentry) (257889f) into [master](https://codecov.io/gh/getsentry/responses/commit/0d2c4f9543fcdf445c8a838ba1b756f007fac02d?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getsentry) (0d2c4f9) will **not change** coverage.
> The diff coverage is `100.00%`.

[![Impacted file tree graph](https://codecov.io/gh/getsentry/responses/pull/489/graphs/tree.svg?width=650&height=150&src=pr&token=x2tAGR7rxE&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getsentry)](https://app.codecov.io/gh/getsentry/responses/compare/489?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getsentry)

```diff
@@            Coverage Diff            @@
##            master      #489   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files            6         6           
  Lines         2075      2070    -5     
=========================================
- Hits          2075      2070    -5     
```


| [Impacted Files](https://app.codecov.io/gh/getsentry/responses/compare/489?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getsentry) | Coverage Δ | |
|---|---|---|
| [responses/\_\_init\_\_.py](https://app.codecov.io/gh/getsentry/responses/compare/489/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getsentry#diff-cmVzcG9uc2VzL19faW5pdF9fLnB5) | `100.00% <ø> (ø)` | |
| [responses/test\_matchers.py](https://app.codecov.io/gh/getsentry/responses/compare/489/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getsentry#diff-cmVzcG9uc2VzL3Rlc3RfbWF0Y2hlcnMucHk=) | `100.00% <ø> (ø)` | |
| [responses/test\_responses.py](https://app.codecov.io/gh/getsentry/responses/compare/489/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getsentry#diff-cmVzcG9uc2VzL3Rlc3RfcmVzcG9uc2VzLnB5) | `100.00% <100.00%> (ø)` | |

------

[Continue to review full report at Codecov](https://app.codecov.io/gh/getsentry/responses/compare/489?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getsentry).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getsentry)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://app.codecov.io/gh/getsentry/responses/compare/489?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getsentry). Last update [0d2c4f9...257889f](https://app.codecov.io/gh/getsentry/responses/compare/489?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getsentry). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getsentry).


Patch:
diff --git a/CHANGES b/CHANGES
index 8a4d3df..57ecf16 100644
--- a/CHANGES
+++ b/CHANGES
@@ -4,6 +4,8 @@
 * Expose `get_registry()` method of `RequestsMock` object. Replaces internal `_get_registry()`.
 * `query_param_matcher` can now accept dictionaries with `int` and `float` values.
 * Added support for `async/await` functions.
+* `response_callback` is no longer executed on exceptions raised by failed `Response`s
+* An error is now raised when both `content_type` and `headers[content-type]` are provided as parameters.
 
 0.18.0
 ------
diff --git a/responses/__init__.py b/responses/__init__.py
index 79dbc84..343263c 100644
--- a/responses/__init__.py
+++ b/responses/__init__.py
@@ -323,7 +323,7 @@ class Response(BaseResponse):
         stream=None,
         content_type=_UNSET,
         auto_calculate_content_length=False,
-        **kwargs
+        **kwargs,
     ):
         # if we were passed a `json` argument,
         # override the body and content_type
@@ -522,7 +522,7 @@ class RequestsMock(object):
         body="",
         adding_headers=None,
         *args,
-        **kwargs
+        **kwargs,
     ):
         """
         >>> import responses
@@ -558,6 +558,13 @@ class RequestsMock(object):
 
         if adding_headers is not None:
             kwargs.setdefault("headers", adding_headers)
+        if "content_type" in kwargs and "headers" in kwargs:
+            header_keys = [header.lower() for header in kwargs["headers"]]
+            if "content-type" in header_keys:
+                raise RuntimeError(
+                    "You cannot define both `content_type` and `headers[Content-Type]`."
+                    " Using the `content_type` kwarg is recommended."
+                )
 
         self._registry.add(Response(method=method, url=url, body=body, **kwargs))
 
@@ -721,8 +728,8 @@ class RequestsMock(object):
                 "Connection refused by Responses - the call doesn't "
                 "match any registered mock.\n\n"
                 "Request: \n"
-                "- %s %s\n\n"
-                "Available matches:\n" % (request.method, request.url)
+                f"- {request.method} {request.url}\n\n"
+                "Available matches:\n"
             )
             for i, m in enumerate(self.registered()):
                 error_msg += "- {} {} {}\n".format(
@@ -733,7 +740,6 @@ class RequestsMock(object):
             response.request = request
 
             self._calls.add(request, response)
-            response = resp_callback(response) if resp_callback else response
             raise response
 
         if match.passthrough:
@@ -745,7 +751,6 @@ class RequestsMock(object):
             except BaseException as response:
                 match.call_count += 1
                 self._calls.add(request, response)
-                response = resp_callback(response) if resp_callback else response
                 raise
 
         response = resp_callback(response) if resp_callback else response
@@ -788,9 +793,7 @@ class RequestsMock(object):
             return True
         else:
             raise AssertionError(
-                "Expected URL '{0}' to be called {1} times. Called {2} times.".format(
-                    url, count, call_count
-                )
+                f"Expected URL '{url}' to be called {count} times. Called {call_count} times."
             )
 
 
diff --git a/responses/matchers.py b/responses/matchers.py
index 1fee6a5..d2a041b 100644
--- a/responses/matchers.py
+++ b/responses/matchers.py
@@ -1,12 +1,14 @@
 import json as json_module
+from typing import Dict, Any, Optional, Callable, Tuple, Union, List
 
 from requests import PreparedRequest
 from urllib.parse import parse_qsl, urlparse
-from requests.packages.urllib3.util.url import parse_url
+
+from requests.packages.urllib3.util.url import parse_url  # type: ignore[import]
 from json.decoder import JSONDecodeError
 
 
-def _create_key_val_str(input_dict):
+def _create_key_val_str(input_dict: Union[Dict[Any, Any], Any]) -> str:
     """
     Returns string of format {'key': val, 'key2': val2}
     Function is called recursively for nested dictionaries
@@ -15,7 +17,7 @@ def _create_key_val_str(input_dict):
     :return: (str) reformatted string
     """
 
-    def list_to_str(input_list):
+    def list_to_str(input_list: List[str]) -> str:
         """
         Convert all list items to string.
         Function is called recursively for nested lists
@@ -45,7 +47,7 @@ def _create_key_val_str(input_dict):
     return key_val_str
 
 
-def urlencoded_params_matcher(params):
+def urlencoded_params_matcher(params: Optional[Dict[str, str]]) -> Callable[..., Any]:
     """
     Matches URL encoded data
 
@@ -53,10 +55,10 @@ def urlencoded_params_matcher(params):
     :return: (func) matcher
     """
 
-    def match(request):
+    def match(request: PreparedRequest) -> Tuple[bool, str]:
         reason = ""
         request_body = request.body
-        qsl_body = dict(parse_qsl(request_body)) if request_body else {}
+        qsl_body = dict(parse_qsl(request_body)) if request_body else {}  # type: ignore[type-var]
         params_dict = params or {}
         valid = params is None if request_body is None else params_dict == qsl_body
         if not valid:
@@ -69,7 +71,7 @@ def urlencoded_params_matcher(params):
     return match
 
 
-def json_params_matcher(params):
+def json_params_matcher(params: Optional[Dict[str, Any]]) -> Callable[..., Any]:
     """
     Matches JSON encoded data
 
@@ -77,7 +79,7 @@ def json_params_matcher(params):
     :return: (func) matcher
     """
 
-    def match(request):
+    def match(request: PreparedRequest) -> Tuple[bool, str]:
         reason = ""
         request_body = request.body
         params_dict = params or {}
@@ -104,27 +106,29 @@ def json_params_matcher(params):
     return match
 
 
-def fragment_identifier_matcher(identifier):
-    def match(request):
+def fragment_identifier_matcher(identifier: Optional[str]) -> Callable[..., Any]:
+    def match(request: PreparedRequest) -> Tuple[bool, str]:
         reason = ""
         url_fragment = urlparse(request.url).fragment
         if identifier:
-            url_fragment_qsl = sorted(parse_qsl(url_fragment))
+            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:
             valid = not url_fragment
 
         if not valid:
-            reason = "URL fragment identifier is different: {} doesn't match {}".format(
-                identifier, url_fragment
+            reason = (
+                "URL fragment identifier is different: "  # type: ignore[str-bytes-safe]
+                f"{identifier} doesn't match {url_fragment}"
             )
+
         return valid, reason
 
     return match
 
 
-def query_param_matcher(params):
+def query_param_matcher(params: Optional[Dict[str, Any]]) -> Callable[..., Any]:
     """
     Matcher to match 'params' argument in request
 
@@ -132,15 +136,16 @@ def query_param_matcher(params):
     :return: (func) matcher
     """
 
-    for k, v in params.items():
+    params_dict = params or {}
+
+    for k, v in params_dict.items():
         if isinstance(v, (int, float)):
-            params[k] = str(v)
+            params_dict[k] = str(v)
 
-    def match(request):
+    def match(request: PreparedRequest) -> Tuple[bool, str]:
         reason = ""
-        request_params = request.params
+        request_params = request.params  # type: ignore[attr-defined]
         request_params_dict = request_params or {}
-        params_dict = params or {}
         valid = (
             params is None
             if request_params is None
@@ -158,7 +163,7 @@ def query_param_matcher(params):
     return match
 
 
-def query_string_matcher(query):
+def query_string_matcher(query: Optional[str]) -> Callable[..., Any]:
     """
     Matcher to match query string part of request
 
@@ -166,7 +171,7 @@ def query_string_matcher(query):
     :return: (func) matcher
     """
 
-    def match(request):
+    def match(request: PreparedRequest) -> Tuple[bool, str]:
         reason = ""
         data = parse_url(request.url)
         request_query = data.query
@@ -187,7 +192,7 @@ def query_string_matcher(query):
     return match
 
 
-def request_kwargs_matcher(kwargs):
+def request_kwargs_matcher(kwargs: Optional[Dict[str, Any]]) -> Callable[..., Any]:
     """
     Matcher to match keyword arguments provided to request
 
@@ -195,18 +200,17 @@ def request_kwargs_matcher(kwargs):
     :return: (func) matcher
     """
 
-    def match(request):
+    def match(request: PreparedRequest) -> Tuple[bool, str]:
         reason = ""
         kwargs_dict = kwargs or {}
         # validate only kwargs that were requested for comparison, skip defaults
-        request_kwargs = {
-            k: v for k, v in request.req_kwargs.items() if k in kwargs_dict
-        }
+        req_kwargs = request.req_kwargs  # type: ignore[attr-defined]
+        request_kwargs = {k: v for k, v in req_kwargs.items() if k in kwargs_dict}
 
         valid = (
             not kwargs_dict
             if not request_kwargs
-            else sorted(kwargs.items()) == sorted(request_kwargs.items())
+            else sorted(kwargs_dict.items()) == sorted(request_kwargs.items())
         )
 
         if not valid:
@@ -219,7 +223,9 @@ def request_kwargs_matcher(kwargs):
     return match
 
 
-def multipart_matcher(files, data=None):
+def multipart_matcher(
+    files: Dict[str, Any], data: Optional[Dict[str, str]] = None
+) -> Callable[..., Any]:
     """
     Matcher to match 'multipart/form-data' content-type.
     This function constructs request body and headers from provided 'data' and 'files'
@@ -233,10 +239,10 @@ def multipart_matcher(files, data=None):
         raise TypeError("files argument cannot be empty")
 
     prepared = PreparedRequest()
-    prepared.headers = {"Content-Type": ""}
+    prepared.headers = {"Content-Type": ""}  # type: ignore[assignment]
     prepared.prepare_body(data=data, files=files)
 
-    def get_boundary(content_type):
+    def get_boundary(content_type: str) -> str:
         """
         Parse 'boundary' value from header.
 
@@ -248,7 +254,7 @@ def multipart_matcher(files, data=None):
 
         return content_type.split("boundary=")[1]
 
-    def match(request):
+    def match(request: PreparedRequest) -> Tuple[bool, str]:
         reason = "multipart/form-data doesn't match. "
         if "Content-Type" not in request.headers:
             return False, reason + "Request is missing the 'Content-Type' header"
@@ -265,14 +271,16 @@ def multipart_matcher(files, data=None):
         )
 
         request_body = request.body
-        prepared_body = prepared.body
+        prepared_body = prepared.body or ""
 
         if isinstance(prepared_body, bytes):
             # since headers always come as str, need to convert to bytes
-            prepared_boundary = prepared_boundary.encode("utf-8")
-            request_boundary = request_boundary.encode("utf-8")
+            prepared_boundary = prepared_boundary.encode("utf-8")  # type: ignore[assignment]
+            request_boundary = request_boundary.encode("utf-8")  # type: ignore[assignment]
 
-        prepared_body = prepared_body.replace(prepared_boundary, request_boundary)
+        prepared_body = prepared_body.replace(
+            prepared_boundary, request_boundary  # type: ignore[arg-type]
+        )
 
         headers_valid = prepared_content_type == request_content_type
         if not headers_valid:
@@ -286,8 +294,12 @@ def multipart_matcher(files, data=None):
 
         body_valid = prepared_body == request_body
         if not body_valid:
-            return False, reason + "Request body differs. {} aren't equal {}".format(
-                request_body, prepared_body
+            return (
+                False,
+                reason
+                + "Request body differs. {} aren't equal {}".format(  # type: ignore[str-bytes-safe]
+                    request_body, prepared_body
+                ),
             )
 
         return True, ""
@@ -295,7 +307,9 @@ def multipart_matcher(files, data=None):
     return match
 
 
-def header_matcher(headers, strict_match=False):
+def header_matcher(
+    headers: Dict[str, str], strict_match: bool = False
+) -> Callable[..., Any]:
     """
     Matcher to match 'headers' argument in request using the responses library.
 
@@ -310,8 +324,8 @@ def header_matcher(headers, strict_match=False):
     :return: (func) matcher
     """
 
-    def match(request):
-        request_headers = request.headers or {}
+    def match(request: PreparedRequest) -> Tuple[bool, str]:
+        request_headers: Union[Dict[Any, Any], Any] = request.headers or {}
 
         if not strict_match:
             # filter down to just the headers specified in the matcher
diff --git a/responses/matchers.pyi b/responses/matchers.pyi
deleted file mode 100644
index 408eb55..0000000
--- a/responses/matchers.pyi
+++ /dev/null
@@ -1,44 +0,0 @@
-from typing import (
-    Any,
-    Callable,
-    Optional,
-    Dict,
-)
-
-JSONDecodeError = ValueError
-
-
-def _create_key_val_str(input_dict: Dict[Any, Any]) -> str: ...
-
-def json_params_matcher(
-    params: Optional[Dict[str, Any]]
-) -> Callable[..., Any]: ...
-
-def urlencoded_params_matcher(
-    params: Optional[Dict[str, str]]
-) -> Callable[..., Any]: ...
-
-def query_param_matcher(
-    params: Optional[Dict[str, Any]]
-) -> Callable[..., Any]: ...
-
-def query_string_matcher(
-    query: Optional[str]
-) -> Callable[..., Any]: ...
-
-def request_kwargs_matcher(
-    kwargs: Optional[Dict[str, Any]]
-) -> Callable[..., Any]: ...
-
-def multipart_matcher(
-        files: Dict[str, Any], data: Optional[Dict[str, str]] = ...
-) -> Callable[..., Any]: ...
-
-def header_matcher(
-    headers: Dict[str, str],
-    strict_match: bool = ...
-) -> Callable[..., Any]: ...
-
-def fragment_identifier_matcher(
-    identifier: Optional[str]
-) -> Callable[..., Any]: ...


Test patch:
diff --git a/responses/test_matchers.py b/responses/test_matchers.py
index 937e8c0..a32969b 100644
--- a/responses/test_matchers.py
+++ b/responses/test_matchers.py
@@ -1,5 +1,3 @@
-from __future__ import absolute_import, print_function, division, unicode_literals
-
 import pytest
 import requests
 import responses
diff --git a/responses/test_responses.py b/responses/test_responses.py
index a6ef490..ee4b2ce 100644
--- a/responses/test_responses.py
+++ b/responses/test_responses.py
@@ -1,7 +1,5 @@
 # coding: utf-8
 
-from __future__ import absolute_import, print_function, division, unicode_literals
-
 import inspect
 import os
 import re
@@ -19,11 +17,7 @@ from responses import (
     CallbackResponse,
 )
 
-
-try:
-    from mock import patch, Mock
-except ImportError:
-    from unittest.mock import patch, Mock  # type: ignore
+from unittest.mock import patch, Mock
 
 
 def assert_reset():
@@ -797,7 +791,7 @@ def test_custom_adapter():
         class DummyAdapter(requests.adapters.HTTPAdapter):
             def send(self, *a, **k):
                 calls[0] += 1
-                return super(DummyAdapter, self).send(*a, **k)
+                return super().send(*a, **k)
 
         # Test that the adapter is actually used
         session = requests.Session()
@@ -1319,6 +1313,26 @@ def test_legacy_adding_headers():
     assert_reset()
 
 
+def test_legacy_adding_headers_with_content_type():
+    @responses.activate
+    def run():
+        with pytest.raises(RuntimeError) as excinfo:
+            responses.add(
+                responses.GET,
+                "http://example.com",
+                body="test",
+                content_type="text/html",
+                adding_headers={"Content-Type": "text/html; charset=utf-8"},
+            )
+        assert (
+            "You cannot define both `content_type` and `headers[Content-Type]`"
+            in str(excinfo.value)
+        )
+
+    run()
+    assert_reset()
+
+
 def test_auto_calculate_content_length_string_body():
     @responses.activate
     def run():