Repository: getsentry/responses

Issue:
`assert_call_count` should be able to check urls without matching querystring
When adding an url in `responses`, querystring is not matched by default:

```python
@responses.activate
def test_my_test():
    responses.add(responses.GET, "http://www.example.com")
    # Both these requests will be captured
    requests.get("http://www.example.com")
    requests.get("http://www.example.com?hello=world")
```

`assert_call_count` doesn't reproduce this behaviour:
```python
@responses.activate
def test_assert_call_count_always_match_qs():
    responses.add(responses.GET, "http://www.example.com")
    requests.get("http://www.example.com")
    requests.get("http://www.example.com?hello=world")

    # One call on each url, querystring is matched by default
    responses.assert_call_count("http://www.example.com", 1) is True
    responses.assert_call_count("http://www.example.com?hello=world", 1) is True
```

I would like to be able to check the call count on one particular url without matching querystring, eg.:

```python
@responses.activate
def test_assert_call_count_match_querystring():
    responses.add(responses.GET, "http://www.example.com")
    requests.get("http://www.example.com")
    requests.get("http://www.example.com?hello=world")

    responses.assert_call_count("http://www.example.com", 2, match_querystring=False) is True
    responses.assert_call_count("http://www.example.com?hello=world", 2, match_querystring=False) is True
```

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

```diff
@@            Coverage Diff            @@
##            master      #529   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files            6         6           
  Lines         2320      2352   +32     
=========================================
+ Hits          2320      2352   +32     
```


| [Impacted Files](https://app.codecov.io/gh/getsentry/responses/compare/529?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/529/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% <100.00%> (ø)` | |
| [responses/registries.py](https://app.codecov.io/gh/getsentry/responses/compare/529/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getsentry#diff-cmVzcG9uc2VzL3JlZ2lzdHJpZXMucHk=) | `100.00% <100.00%> (ø)` | |
| [responses/tests/test\_responses.py](https://app.codecov.io/gh/getsentry/responses/compare/529/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getsentry#diff-cmVzcG9uc2VzL3Rlc3RzL3Rlc3RfcmVzcG9uc2VzLnB5) | `100.00% <100.00%> (ø)` | |

------

[Continue to review full report at Codecov](https://app.codecov.io/gh/getsentry/responses/compare/529?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/529?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getsentry). Last update [9fedd77...15a68a7](https://app.codecov.io/gh/getsentry/responses/compare/529?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 fc3be1c..e916719 100644
--- a/CHANGES
+++ b/CHANGES
@@ -1,7 +1,10 @@
 0.21.0
 ------
 
+* Add `threading.Lock()` to allow `responses` working with `threading` module.
 * Removed internal `_cookies_from_headers` function
+* Now `add`, `upsert`, `replace` methods return registered response.
+  `remove` method returns list of removed responses.
 
 0.20.0
 ------
diff --git a/README.rst b/README.rst
index 445ab4a..94b960f 100644
--- a/README.rst
+++ b/README.rst
@@ -517,6 +517,11 @@ you can see, that status code will depend on the invocation order.
 
 .. code-block:: python
 
+    import requests
+
+    import responses
+    from responses.registries import OrderedRegistry
+
     @responses.activate(registry=OrderedRegistry)
     def test_invocation_index():
         responses.add(
@@ -800,6 +805,42 @@ the ``assert_all_requests_are_fired`` value:
 Assert Request Call Count
 -------------------------
 
+Assert based on ``Response`` object
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+Each ``Response`` object has ``call_count`` attribute that could be inspected
+to check how many times each request was matched.
+
+.. code-block:: python
+
+    @responses.activate
+    def test_call_count_with_matcher():
+
+        rsp = responses.add(
+            responses.GET,
+            "http://www.example.com",
+            match=(matchers.query_param_matcher({}),),
+        )
+        rsp2 = responses.add(
+            responses.GET,
+            "http://www.example.com",
+            match=(matchers.query_param_matcher({"hello": "world"}),),
+            status=777,
+        )
+        requests.get("http://www.example.com")
+        resp1 = requests.get("http://www.example.com")
+        requests.get("http://www.example.com?hello=world")
+        resp2 = requests.get("http://www.example.com?hello=world")
+
+        assert resp1.status_code == 200
+        assert resp2.status_code == 777
+
+        assert rsp.call_count == 2
+        assert rsp2.call_count == 2
+
+Assert based on the exact URL
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
 Assert that the request was called exactly n times.
 
 .. code-block:: python
@@ -819,6 +860,16 @@ Assert that the request was called exactly n times.
             responses.assert_call_count("http://example.com", 1)
         assert "Expected URL 'http://example.com' to be called 1 times. Called 2 times." in str(excinfo.value)
 
+    @responses.activate
+    def test_assert_call_count_always_match_qs():
+        responses.add(responses.GET, "http://www.example.com")
+        requests.get("http://www.example.com")
+        requests.get("http://www.example.com?hello=world")
+
+        # One call on each url, querystring is matched by default
+        responses.assert_call_count("http://www.example.com", 1) is True
+        responses.assert_call_count("http://www.example.com?hello=world", 1) is True
+
 
 Multiple Responses
 ------------------
@@ -1010,6 +1061,32 @@ matched responses from the registered list.
 
 Finally, ``reset`` will reset all registered responses.
 
+Coroutines and Multithreading
+-----------------------------
+
+``responses`` supports both Coroutines and Multithreading out of the box.
+Note, ``responses`` locks threading on ``RequestMock`` object allowing only
+single thread to access it.
+
+.. code-block:: python
+
+    async def test_async_calls():
+        @responses.activate
+        async def run():
+            responses.add(
+                responses.GET,
+                "http://twitter.com/api/1/foobar",
+                json={"error": "not found"},
+                status=404,
+            )
+
+            resp = requests.get("http://twitter.com/api/1/foobar")
+            assert resp.json() == {"error": "not found"}
+            assert responses.calls[0].request.url == "http://twitter.com/api/1/foobar"
+
+        await run()
+
+
 Contributing
 ------------
 
diff --git a/responses/__init__.py b/responses/__init__.py
index e72703c..460d24e 100644
--- a/responses/__init__.py
+++ b/responses/__init__.py
@@ -8,6 +8,14 @@ from functools import wraps
 from http import client
 from itertools import groupby
 from re import Pattern
+from threading import Lock as _ThreadingLock
+from typing import TYPE_CHECKING
+from typing import Any
+from typing import Callable
+from typing import Dict
+from typing import Iterator
+from typing import Optional
+from typing import Union
 from warnings import warn
 
 from requests.adapters import HTTPAdapter
@@ -40,6 +48,13 @@ from urllib.parse import urlsplit
 from urllib.parse import urlunparse
 from urllib.parse import urlunsplit
 
+if TYPE_CHECKING:  # pragma: no cover
+    # import only for linter run
+    from requests import PreparedRequest
+
+# Block of type annotations
+_Body = Union[str, BaseException, "Response", BufferedReader, bytes]
+
 Call = namedtuple("Call", ["request", "response"])
 _real_send = HTTPAdapter.send
 _UNSET = object()
@@ -54,13 +69,13 @@ class FalseBool:
     https://github.com/getsentry/responses/issues/464
     """
 
-    def __bool__(self):
+    def __bool__(self) -> bool:
         return False
 
     __nonzero__ = __bool__
 
 
-def urlencoded_params_matcher(params):
+def urlencoded_params_matcher(params: Optional[Dict[str, str]]) -> Callable[..., Any]:
     warn(
         "Function is deprecated. Use 'from responses.matchers import urlencoded_params_matcher'",
         DeprecationWarning,
@@ -68,7 +83,7 @@ def urlencoded_params_matcher(params):
     return _urlencoded_params_matcher(params)
 
 
-def json_params_matcher(params):
+def json_params_matcher(params: Optional[Dict[str, Any]]) -> Callable[..., Any]:
     warn(
         "Function is deprecated. Use 'from responses.matchers import json_params_matcher'",
         DeprecationWarning,
@@ -76,11 +91,11 @@ def json_params_matcher(params):
     return _json_params_matcher(params)
 
 
-def _has_unicode(s):
+def _has_unicode(s: str) -> bool:
     return any(ord(char) > 128 for char in s)
 
 
-def _clean_unicode(url):
+def _clean_unicode(url: str) -> str:
     """Clean up URLs, which use punycode to handle unicode chars.
 
     Applies percent encoding to URL path and query if required.
@@ -116,7 +131,9 @@ def _clean_unicode(url):
     return "".join(chars)
 
 
-def get_wrapped(func, responses, registry=None):
+def get_wrapped(
+    func: Callable[..., Any], responses: "RequestsMock", registry: Optional[Any] = None
+) -> Callable[..., Any]:
     """Wrap provided function inside ``responses`` context manager.
 
     Provides a synchronous or asynchronous wrapper for the function.
@@ -158,26 +175,28 @@ def get_wrapped(func, responses, registry=None):
 
 
 class CallList(Sequence, Sized):
-    def __init__(self):
+    def __init__(self) -> None:
         self._calls = []
 
-    def __iter__(self):
+    def __iter__(self) -> Iterator[Call]:
         return iter(self._calls)
 
-    def __len__(self):
+    def __len__(self) -> int:
         return len(self._calls)
 
-    def __getitem__(self, idx):
+    def __getitem__(self, idx: int) -> Call:
         return self._calls[idx]
 
-    def add(self, request, response):
+    def add(self, request: "PreparedRequest", response: _Body) -> None:
         self._calls.append(Call(request, response))
 
-    def reset(self):
+    def reset(self) -> None:
         self._calls = []
 
 
-def _ensure_url_default_path(url):
+def _ensure_url_default_path(
+    url: "Union[Pattern[str], str]",
+) -> "Union[Pattern[str], str]":
     """Add empty URL path '/' if doesn't exist.
 
     Examples
@@ -204,7 +223,7 @@ def _ensure_url_default_path(url):
     return url
 
 
-def _get_url_and_path(url):
+def _get_url_and_path(url: str) -> str:
     """Construct URL only containing scheme, netloc and path by truncating other parts.
 
     This method complies with RFC 3986.
@@ -233,7 +252,9 @@ def _get_url_and_path(url):
     return parse_url(url_and_path).url
 
 
-def _handle_body(body):
+def _handle_body(
+    body: Optional[Union[bytes, BufferedReader, str]]
+) -> Union[BufferedReader, BytesIO]:
     """Generates `Response` body.
 
     Parameters
@@ -569,6 +590,7 @@ class RequestsMock(object):
         self.passthru_prefixes = tuple(passthru_prefixes)
         self.target = target
         self._patcher = None
+        self._thread_lock = _ThreadingLock()
 
     def get_registry(self):
         return self._registry
@@ -626,8 +648,7 @@ class RequestsMock(object):
 
         """
         if isinstance(method, BaseResponse):
-            self._registry.add(method)
-            return
+            return self._registry.add(method)
 
         if adding_headers is not None:
             kwargs.setdefault("headers", adding_headers)
@@ -639,28 +660,29 @@ class RequestsMock(object):
                     " Using the `content_type` kwarg is recommended."
                 )
 
-        self._registry.add(Response(method=method, url=url, body=body, **kwargs))
+        response = Response(method=method, url=url, body=body, **kwargs)
+        return self._registry.add(response)
 
     def delete(self, *args, **kwargs):
-        self.add(DELETE, *args, **kwargs)
+        return self.add(DELETE, *args, **kwargs)
 
     def get(self, *args, **kwargs):
-        self.add(GET, *args, **kwargs)
+        return self.add(GET, *args, **kwargs)
 
     def head(self, *args, **kwargs):
-        self.add(HEAD, *args, **kwargs)
+        return self.add(HEAD, *args, **kwargs)
 
     def options(self, *args, **kwargs):
-        self.add(OPTIONS, *args, **kwargs)
+        return self.add(OPTIONS, *args, **kwargs)
 
     def patch(self, *args, **kwargs):
-        self.add(PATCH, *args, **kwargs)
+        return self.add(PATCH, *args, **kwargs)
 
     def post(self, *args, **kwargs):
-        self.add(POST, *args, **kwargs)
+        return self.add(POST, *args, **kwargs)
 
     def put(self, *args, **kwargs):
-        self.add(PUT, *args, **kwargs)
+        return self.add(PUT, *args, **kwargs)
 
     def add_passthru(self, prefix):
         """
@@ -696,7 +718,7 @@ class RequestsMock(object):
         else:
             response = BaseResponse(method=method_or_response, url=url)
 
-        self._registry.remove(response)
+        return self._registry.remove(response)
 
     def replace(self, method_or_response=None, url=None, body="", *args, **kwargs):
         """
@@ -713,7 +735,7 @@ class RequestsMock(object):
         else:
             response = Response(method=method_or_response, url=url, body=body, **kwargs)
 
-        self._registry.replace(response)
+        return self._registry.replace(response)
 
     def upsert(self, method_or_response=None, url=None, body="", *args, **kwargs):
         """
@@ -726,9 +748,9 @@ class RequestsMock(object):
         >>> responses.upsert(responses.GET, 'http://example.org', json={'data': 2})
         """
         try:
-            self.replace(method_or_response, url, body, *args, **kwargs)
+            return self.replace(method_or_response, url, body, *args, **kwargs)
         except ValueError:
-            self.add(method_or_response, url, body, *args, **kwargs)
+            return self.add(method_or_response, url, body, *args, **kwargs)
 
     def add_callback(
         self,
@@ -786,7 +808,8 @@ class RequestsMock(object):
             (Response) found match. If multiple found, then remove & return the first match.
             (list) list with reasons why other matches don't match
         """
-        return self._registry.find(request)
+        with self._thread_lock:
+            return self._registry.find(request)
 
     def _parse_request_params(self, url):
         params = {}
diff --git a/responses/__init__.pyi b/responses/__init__.pyi
index ab4c26c..89a2bfa 100644
--- a/responses/__init__.pyi
+++ b/responses/__init__.pyi
@@ -1,11 +1,9 @@
 from collections import Sequence
 from collections import Sized
 from io import BufferedReader
-from io import BytesIO
 from re import Pattern
 from typing import Any
 from typing import Callable
-from typing import Dict
 from typing import Iterable
 from typing import Iterator
 from typing import List
@@ -23,7 +21,6 @@ from urllib.parse import quote as quote
 
 from requests.adapters import HTTPResponse
 from requests.adapters import PreparedRequest
-from requests.cookies import RequestsCookieJar
 from typing_extensions import Literal
 
 # Not currently exposed in typestubs, thus, ignore
@@ -32,21 +29,6 @@ from urllib3.response import HTTPHeaderDict  # type: ignore
 from .matchers import json_params_matcher
 from .matchers import urlencoded_params_matcher
 
-def _clean_unicode(url: str) -> str: ...
-def _ensure_str(s: str) -> str: ...
-def _ensure_url_default_path(
-    url: Union[Pattern[str], str]
-) -> Union[Pattern[str], str]: ...
-def _get_url_and_path(url: str) -> str: ...
-def _handle_body(
-    body: Optional[Union[bytes, BufferedReader, str]]
-) -> Union[BufferedReader, BytesIO]: ...
-def _has_unicode(s: str) -> bool: ...
-def _is_string(s: Union[Pattern[str], str]) -> bool: ...
-def get_wrapped(
-    func: Callable[..., Any], responses: RequestsMock, registry: Optional[Any]
-) -> Callable[..., Any]: ...
-
 class Call(NamedTuple):
     request: PreparedRequest
     response: Any
@@ -228,7 +210,7 @@ class _Add(Protocol):
         adding_headers: HeaderSet = ...,
         match_querystring: bool = ...,
         match: MatcherIterable = ...,
-    ) -> None: ...
+    ) -> BaseResponse: ...
 
 class _Shortcut(Protocol):
     def __call__(
@@ -244,7 +226,7 @@ class _Shortcut(Protocol):
         adding_headers: HeaderSet = ...,
         match_querystring: bool = ...,
         match: MatcherIterable = ...,
-    ) -> None: ...
+    ) -> BaseResponse: ...
 
 class _AddCallback(Protocol):
     def __call__(
@@ -267,7 +249,7 @@ class _Remove(Protocol):
         self,
         method_or_response: Optional[Union[str, BaseResponse]] = ...,
         url: Optional[Union[Pattern[str], str]] = ...,
-    ) -> None: ...
+    ) -> List[BaseResponse]: ...
 
 class _Replace(Protocol):
     def __call__(
@@ -283,7 +265,7 @@ class _Replace(Protocol):
         adding_headers: HeaderSet = ...,
         match_querystring: bool = ...,
         match: MatcherIterable = ...,
-    ) -> None: ...
+    ) -> BaseResponse: ...
 
 class _Upsert(Protocol):
     def __call__(
@@ -299,7 +281,7 @@ class _Upsert(Protocol):
         adding_headers: HeaderSet = ...,
         match_querystring: bool = ...,
         match: MatcherIterable = ...,
-    ) -> None: ...
+    ) -> BaseResponse: ...
 
 class _Registered(Protocol):
     def __call__(self) -> List[Response]: ...
diff --git a/responses/registries.py b/responses/registries.py
index 9bad274..049df23 100644
--- a/responses/registries.py
+++ b/responses/registries.py
@@ -46,19 +46,24 @@ class FirstMatchRegistry(object):
                 match_failed_reasons.append(reason)
         return found_match, match_failed_reasons
 
-    def add(self, response: "BaseResponse") -> None:
-        if response in self.registered:
-            # if user adds multiple responses that reference the same instance
+    def add(self, response: "BaseResponse") -> "BaseResponse":
+        if any(response is resp for resp in self.registered):
+            # if user adds multiple responses that reference the same instance.
+            # do a comparison by memory allocation address.
             # see https://github.com/getsentry/responses/issues/479
             response = copy.deepcopy(response)
 
         self.registered.append(response)
+        return response
 
-    def remove(self, response: "BaseResponse") -> None:
+    def remove(self, response: "BaseResponse") -> List["BaseResponse"]:
+        removed_responses = []
         while response in self.registered:
             self.registered.remove(response)
+            removed_responses.append(response)
+        return removed_responses
 
-    def replace(self, response: "BaseResponse") -> None:
+    def replace(self, response: "BaseResponse") -> "BaseResponse":
         try:
             index = self.registered.index(response)
         except ValueError:
@@ -66,6 +71,7 @@ class FirstMatchRegistry(object):
                 "Response is not registered for URL {}".format(response.url)
             )
         self.registered[index] = response
+        return response
 
 
 class OrderedRegistry(FirstMatchRegistry):


Test patch:
diff --git a/responses/tests/test_matchers.py b/responses/tests/test_matchers.py
index 80b831b..b7c6191 100644
--- a/responses/tests/test_matchers.py
+++ b/responses/tests/test_matchers.py
@@ -4,18 +4,8 @@ from requests.exceptions import ConnectionError
 
 import responses
 from responses import matchers
-
-
-def assert_response(resp, body=None, content_type="text/plain"):
-    assert resp.status_code == 200
-    assert resp.reason == "OK"
-    assert resp.headers["Content-Type"] == content_type
-    assert resp.text == body
-
-
-def assert_reset():
-    assert len(responses._default_mock.registered()) == 0
-    assert len(responses.calls) == 0
+from responses.tests.test_responses import assert_reset
+from responses.tests.test_responses import assert_response
 
 
 def test_query_string_matcher():
@@ -187,7 +177,7 @@ def test_request_matches_empty_body():
             )
 
             with pytest.raises(ConnectionError) as excinfo:
-                resp = requests.request(
+                requests.request(
                     "POST",
                     "http://example.com/",
                     json={"my": "data"},
@@ -205,7 +195,7 @@ def test_request_matches_empty_body():
                 match=[matchers.urlencoded_params_matcher(None)],
             )
             with pytest.raises(ConnectionError) as excinfo:
-                resp = requests.request(
+                requests.request(
                     "POST",
                     "http://example.com/",
                     headers={"Content-Type": "x-www-form-urlencoded"},
diff --git a/responses/tests/test_multithreading.py b/responses/tests/test_multithreading.py
new file mode 100644
index 0000000..39f9490
--- /dev/null
+++ b/responses/tests/test_multithreading.py
@@ -0,0 +1,35 @@
+"""
+Separate file for multithreading since it takes time to run
+"""
+import threading
+
+import pytest
+import requests
+
+import responses
+
+
+@pytest.mark.parametrize("execution_number", range(10))
+def test_multithreading_lock(execution_number):
+    """Reruns test multiple times since error is random and
+    depends on CPU and can lead to false positive result.
+
+    """
+    n_threads = 10
+    n_requests = 30
+    with responses.RequestsMock() as m:
+        for j in range(n_threads):
+            for i in range(n_requests):
+                m.add(url=f"http://example.com/example{i}", method="GET")
+
+        def fun():
+            for req in range(n_requests):
+                requests.get(f"http://example.com/example{req}")
+
+        threads = [
+            threading.Thread(name=f"example{i}", target=fun) for i in range(n_threads)
+        ]
+        for thread in threads:
+            thread.start()
+        for thread in threads:
+            thread.join()
diff --git a/responses/tests/test_responses.py b/responses/tests/test_responses.py
index 2ca8c8a..47479bb 100644
--- a/responses/tests/test_responses.py
+++ b/responses/tests/test_responses.py
@@ -6,6 +6,8 @@ import re
 import warnings
 from io import BufferedReader
 from io import BytesIO
+from typing import Any
+from typing import Optional
 from unittest.mock import Mock
 from unittest.mock import patch
 
@@ -24,11 +26,13 @@ from responses import matchers
 
 
 def assert_reset():
-    assert len(responses._default_mock.registered()) == 0
+    assert len(responses.mock.registered()) == 0
     assert len(responses.calls) == 0
 
 
-def assert_response(resp, body=None, content_type="text/plain"):
+def assert_response(
+    resp: Any, body: Optional[Any] = None, content_type: "Optional[str]" = "text/plain"
+) -> None:
     assert resp.status_code == 200
     assert resp.reason == "OK"
     if content_type is not None:
@@ -612,7 +616,7 @@ def test_callback_exception_result():
     result = Exception()
     url = "http://example.com/"
 
-    def request_callback(request):
+    def request_callback(_request):
         return result
 
     @responses.activate
@@ -632,7 +636,7 @@ def test_callback_exception_body():
     body = Exception()
     url = "http://example.com/"
 
-    def request_callback(request):
+    def request_callback(_request):
         return 200, {}, body
 
     @responses.activate
@@ -673,7 +677,7 @@ def test_callback_no_content_type():
 
 
 def test_callback_content_type_dict():
-    def request_callback(request):
+    def request_callback(_request):
         return (
             200,
             {"Content-Type": "application/json"},
@@ -692,7 +696,7 @@ def test_callback_content_type_dict():
 
 
 def test_callback_matchers():
-    def request_callback(request):
+    def request_callback(_request):
         return (
             200,
             {"Content-Type": "application/json"},
@@ -748,7 +752,7 @@ def test_callback_matchers_fail():
 
 
 def test_callback_content_type_tuple():
-    def request_callback(request):
+    def request_callback(_request):
         return (
             200,
             [("Content-Type", "application/json")],
@@ -811,7 +815,7 @@ def test_custom_adapter():
         session = requests.Session()
         session.mount("http://", DummyAdapter())
 
-        resp = session.get(url, allow_redirects=False)
+        session.get(url, allow_redirects=False)
         assert calls[0] == 1
 
         # Test that the response is still correctly emulated
@@ -846,7 +850,7 @@ def test_responses_as_context_manager():
 
 def test_activate_doesnt_change_signature():
     def test_function(a, b=None):
-        return (a, b)
+        return a, b
 
     decorated_test_function = responses.activate(test_function)
     assert inspect.signature(test_function) == inspect.signature(
@@ -918,7 +922,7 @@ def test_activate_doesnt_change_signature_with_return_type():
 def test_activate_doesnt_change_signature_for_method():
     class TestCase(object):
         def test_function(self, a, b=None):
-            return (self, a, b)
+            return self, a, b
 
         decorated_test_function = responses.activate(test_function)
 
@@ -933,8 +937,8 @@ def test_response_cookies():
     headers = {"set-cookie": "session_id=12345; a=b; c=d"}
     url = "http://example.com/"
 
-    def request_callback(request):
-        return (status, headers, body)
+    def request_callback(_request):
+        return status, headers, body
 
     @responses.activate
     def run():
@@ -944,7 +948,7 @@ def test_response_cookies():
         assert resp.status_code == status
         assert "session_id" in resp.cookies
         assert resp.cookies["session_id"] == "12345"
-        assert set(resp.cookies.keys()) == set(["session_id"])
+        assert set(resp.cookies.keys()) == {"session_id"}
 
     run()
     assert_reset()
@@ -956,8 +960,8 @@ def test_response_cookies_secure():
     headers = {"set-cookie": "session_id=12345; a=b; c=d; secure"}
     url = "http://example.com/"
 
-    def request_callback(request):
-        return (status, headers, body)
+    def request_callback(_request):
+        return status, headers, body
 
     @responses.activate
     def run():
@@ -967,7 +971,7 @@ def test_response_cookies_secure():
         assert resp.status_code == status
         assert "session_id" in resp.cookies
         assert resp.cookies["session_id"] == "12345"
-        assert set(resp.cookies.keys()) == set(["session_id"])
+        assert set(resp.cookies.keys()) == {"session_id"}
 
     run()
     assert_reset()
@@ -982,8 +986,8 @@ def test_response_cookies_multiple():
     ]
     url = "http://example.com/"
 
-    def request_callback(request):
-        return (status, headers, body)
+    def request_callback(_request):
+        return status, headers, body
 
     @responses.activate
     def run():
@@ -991,7 +995,7 @@ def test_response_cookies_multiple():
         resp = requests.get(url)
         assert resp.text == "test callback"
         assert resp.status_code == status
-        assert set(resp.cookies.keys()) == set(["1P_JAR", "NID"])
+        assert set(resp.cookies.keys()) == {"1P_JAR", "NID"}
         assert resp.cookies["1P_JAR"] == "2019-12-31-23"
         assert resp.cookies["NID"] == "some=value"
 
@@ -1021,11 +1025,11 @@ def test_response_cookies_session(request_stream, responses_stream):
 
         assert "mycookie" in resp.cookies
         assert resp.cookies["mycookie"] == "cookieval"
-        assert set(resp.cookies.keys()) == set(["mycookie"])
+        assert set(resp.cookies.keys()) == {"mycookie"}
 
         assert "mycookie" in session.cookies
         assert session.cookies["mycookie"] == "cookieval"
-        assert set(session.cookies.keys()) == set(["mycookie"])
+        assert set(session.cookies.keys()) == {"mycookie"}
 
     run()
     assert_reset()
@@ -1035,9 +1039,9 @@ def test_response_callback():
     """adds a callback to decorate the response, then checks it"""
 
     def run():
-        def response_callback(resp):
-            resp._is_mocked = True
-            return resp
+        def response_callback(response):
+            response._is_mocked = True
+            return response
 
         with responses.RequestsMock(response_callback=response_callback) as m:
             m.add(responses.GET, "http://example.com", body=b"test")
@@ -1079,7 +1083,7 @@ def test_use_stream_twice_to_double_raw_io():
 
 
 def test_assert_all_requests_are_fired():
-    def request_callback(request):
+    def request_callback(_request):
         raise BaseException()
 
     def run():
@@ -1694,7 +1698,7 @@ def test_passthru_does_not_persist_across_tests(httpserver):
 
     @responses.activate
     def with_a_passthru():
-        assert not responses._default_mock.passthru_prefixes
+        assert not responses.mock.passthru_prefixes
         responses.add_passthru(re.compile(".*"))
 
         # wrap request that is passed through with another mock. That helps
@@ -1707,7 +1711,7 @@ def test_passthru_does_not_persist_across_tests(httpserver):
 
     @responses.activate
     def without_a_passthru():
-        assert not responses._default_mock.passthru_prefixes
+        assert not responses.mock.passthru_prefixes
         with pytest.raises(requests.exceptions.ConnectionError):
             requests.get("https://example.com")
 
@@ -1831,6 +1835,50 @@ def test_assert_call_count(url):
     assert_reset()
 
 
+def test_call_count_with_matcher():
+    @responses.activate
+    def run():
+        rsp = responses.add(
+            responses.GET,
+            "http://www.example.com",
+            match=(matchers.query_param_matcher({}),),
+        )
+        rsp2 = responses.add(
+            responses.GET,
+            "http://www.example.com",
+            match=(matchers.query_param_matcher({"hello": "world"}),),
+            status=777,
+        )
+        requests.get("http://www.example.com")
+        resp1 = requests.get("http://www.example.com")
+        requests.get("http://www.example.com?hello=world")
+        resp2 = requests.get("http://www.example.com?hello=world")
+
+        assert resp1.status_code == 200
+        assert resp2.status_code == 777
+
+        assert rsp.call_count == 2
+        assert rsp2.call_count == 2
+
+    run()
+    assert_reset()
+
+
+def test_call_count_without_matcher():
+    @responses.activate
+    def run():
+        rsp = responses.add(responses.GET, "http://www.example.com")
+        requests.get("http://www.example.com")
+        requests.get("http://www.example.com")
+        requests.get("http://www.example.com?hello=world")
+        requests.get("http://www.example.com?hello=world")
+
+        assert rsp.call_count == 4
+
+    run()
+    assert_reset()
+
+
 def test_fail_request_error():
     """
     Validate that exception is raised if request URL/Method/kwargs don't match