Repository: getsentry/responses

Issue:
Allow to specify stream=True for callback responses
According to https://github.com/getsentry/responses/blob/master/responses.py#L355 default stream value for callback responses is False.
Also there is no way to override this value using public method add_callback https://github.com/getsentry/responses/blob/master/responses.py#L510
As a result this doesn't work:
```
        def callback(request):
            return 200, {}, 'content'
        responses.add_callback(
            'GET',
            'https://domain.com/',
            callback
        )
        response = requests.get('https://domain.com/', stream=True)
        content = response.raw.read()
....
    response.raw.read()
  File "/usr/lib/python3.6/site-packages/urllib3/response.py", line 380, in read
    data = self._fp.read()
ValueError: I/O operation on closed file.
```
Looks like it can be fixed by adding stream argument to add_callback method

As an workaround one can add a match manually using context manager:
```
        def callback(request):
            return 200, {}, 'content'
        with responses.RequestsMock() as requests_mock:

            requests_mock._matches.append(responses.CallbackResponse(
                'GET',
                'https://domain.com/',
                callback, stream=True
            ))
            response = requests.get('https://domain.com/', stream=True)
            content = response.raw.read()
```

Discussion:

Patch:
diff --git a/CHANGES b/CHANGES
index 60a0617..3505ba9 100644
--- a/CHANGES
+++ b/CHANGES
@@ -3,11 +3,11 @@
 
 * Fixed regression with `stream` parameter deprecation, requests.session() and cookie handling.
 * Replaced adhoc URL parsing with `urllib.parse`.
+* Added ``match`` parameter to ``add_callback`` method
 * Added `responses.matchers.fragment_identifier_matcher`. This matcher allows you
   to match request URL fragment identifier.
 * Improved test coverage.
 
-
 0.15.0
 ------
 
diff --git a/responses/__init__.py b/responses/__init__.py
index 7de3b64..1b1ce57 100644
--- a/responses/__init__.py
+++ b/responses/__init__.py
@@ -259,7 +259,7 @@ class BaseResponse(object):
 
     stream = False
 
-    def __init__(self, method, url, match_querystring=_unspecified, match=[]):
+    def __init__(self, method, url, match_querystring=_unspecified, match=()):
         self.method = method
         # ensure the url has a default path set if the url is a string
         self.url = _ensure_url_default_path(url)
@@ -667,7 +667,13 @@ class RequestsMock(object):
             self.add(method_or_response, url, body, *args, **kwargs)
 
     def add_callback(
-        self, method, url, callback, match_querystring=False, content_type="text/plain"
+        self,
+        method,
+        url,
+        callback,
+        match_querystring=False,
+        content_type="text/plain",
+        match=(),
     ):
         # ensure the url has a default path set if the url is a string
         # url = _ensure_url_default_path(url, match_querystring)
@@ -679,6 +685,7 @@ class RequestsMock(object):
                 callback=callback,
                 content_type=content_type,
                 match_querystring=match_querystring,
+                match=match,
             )
         )
 
diff --git a/responses/__init__.pyi b/responses/__init__.pyi
index 8e0bf35..898c68a 100644
--- a/responses/__init__.pyi
+++ b/responses/__init__.pyi
@@ -11,7 +11,8 @@ from typing import (
     Dict,
     List,
     Tuple,
-    Union
+    Union,
+    Iterable
 )
 from io import BufferedReader, BytesIO
 from re import Pattern
@@ -47,6 +48,8 @@ class Call(NamedTuple):
 
 _Body = Union[str, BaseException, "Response", BufferedReader, bytes]
 
+MatcherIterable = Iterable[Callable[[Any], Callable[..., Any]]]
+
 class CallList(Sequence[Call], Sized):
     def __init__(self) -> None: ...
     def __iter__(self) -> Iterator[Call]: ...
@@ -63,19 +66,19 @@ class BaseResponse:
     method: Any = ...
     url: Any = ...
     match_querystring: Any = ...
-    match: List[Any] = ...
+    match: MatcherIterable = ...
     call_count: int = ...
     def __init__(
         self,
         method: str,
         url: Union[Pattern[str], str],
         match_querystring: Union[bool, object] = ...,
-        match: List[Callable[..., Any]] = ...,
+        match: MatcherIterable = ...,
     ) -> None: ...
     def __eq__(self, other: Any) -> bool: ...
     def __ne__(self, other: Any) -> bool: ...
     def _req_attr_matches(
-        self, match: List[Callable[..., Any]], request: Optional[Union[bytes, str]]
+        self, match: MatcherIterable, request: Optional[Union[bytes, str]]
     ) -> Tuple[bool, str]: ...
     def _should_match_querystring(
         self, match_querystring_argument: Union[bool, object]
@@ -107,7 +110,7 @@ class Response(BaseResponse):
         content_type: Optional[str] = ...,
         auto_calculate_content_length: bool = ...,
         match_querystring: bool = ...,
-        match: List[Any] = ...,
+        match: MatcherIterable = ...,
     ) -> None: ...
     def get_response(  # type: ignore [override]
         self, request: PreparedRequest
@@ -125,7 +128,7 @@ class CallbackResponse(BaseResponse):
         stream: bool = ...,
         content_type: Optional[str] = ...,
         match_querystring: bool = ...,
-        match: List[Any] = ...,
+        match: MatcherIterable = ...,
     ) -> None: ...
     def get_response(  # type: ignore [override]
         self, request: PreparedRequest
@@ -202,7 +205,7 @@ class _Add(Protocol):
         auto_calculate_content_length: bool = ...,
         adding_headers: HeaderSet = ...,
         match_querystring: bool = ...,
-        match: List[Any] = ...,
+        match: MatcherIterable = ...,
     ) -> None: ...
 
 class _AddCallback(Protocol):
@@ -213,6 +216,7 @@ class _AddCallback(Protocol):
         callback: Callable[[PreparedRequest], Union[Exception, Tuple[int, Mapping[str, str], _Body]]],
         match_querystring: bool = ...,
         content_type: Optional[str] = ...,
+        match: MatcherIterable = ...,
     ) -> None: ...
 
 class _AddPassthru(Protocol):
@@ -240,7 +244,7 @@ class _Replace(Protocol):
         content_type: Optional[str] = ...,
         adding_headers: HeaderSet = ...,
         match_querystring: bool = ...,
-        match: List[Any] = ...,
+        match: MatcherIterable = ...,
     ) -> None: ...
 
 class _Upsert(Protocol):
@@ -256,7 +260,7 @@ class _Upsert(Protocol):
         content_type: Optional[str] = ...,
         adding_headers: HeaderSet = ...,
         match_querystring: bool = ...,
-        match: List[Any] = ...,
+        match: MatcherIterable = ...,
     ) -> None: ...
 
 class _Registered(Protocol):


Test patch:
diff --git a/responses/test_responses.py b/responses/test_responses.py
index 9c02fe6..2b8f403 100644
--- a/responses/test_responses.py
+++ b/responses/test_responses.py
@@ -669,6 +669,62 @@ def test_callback_content_type_dict():
     assert_reset()
 
 
+def test_callback_matchers():
+    def request_callback(request):
+        return (
+            200,
+            {"Content-Type": "application/json"},
+            b"foo",
+        )
+
+    @responses.activate
+    def run():
+        req_data = {"some": "other", "data": "fields"}
+        req_files = {"file_name": b"Old World!"}
+
+        responses.add_callback(
+            responses.POST,
+            url="http://httpbin.org/post",
+            match=[matchers.multipart_matcher(req_files, data=req_data)],
+            callback=request_callback,
+        )
+        resp = requests.post("http://httpbin.org/post", data=req_data, files=req_files)
+        assert resp.text == "foo"
+        assert resp.headers["content-type"] == "application/json"
+
+    run()
+    assert_reset()
+
+
+def test_callback_matchers_fail():
+    @responses.activate
+    def run():
+        req_data = {"some": "other", "data": "fields"}
+        req_files = {"file_name": b"Old World!"}
+
+        responses.add_callback(
+            responses.POST,
+            url="http://httpbin.org/post",
+            match=[matchers.multipart_matcher(req_files, data=req_data)],
+            callback=lambda x: (
+                0,
+                {"a": ""},
+                "",
+            ),
+        )
+        with pytest.raises(ConnectionError) as exc:
+            requests.post(
+                "http://httpbin.org/post",
+                data={"some": "other", "data": "wrong"},
+                files=req_files,
+            )
+
+        assert "multipart/form-data doesn't match." in str(exc.value)
+
+    run()
+    assert_reset()
+
+
 def test_callback_content_type_tuple():
     def request_callback(request):
         return (