Repository: getsentry/responses

Issue:
Feature request: allow configuration of passthrough during BaseResponse construction
### Use case

I'm working on a Python test suite that returns mock data for HTTP GET requests, and it has an 'online' mode during which no mocking of requests occurs.

I'd like to migrate the test suite to use `responses`, and would like to retain the online test functionality while also allowing for the assert-on-all-requests logic and other functionality.

This is already achievable using the [`passthrough`](https://github.com/getsentry/responses/tree/3d83a88382bf5cedefbbab3986bbfeeb07883fac#passing-through-real-requests) functionality -- but it's _not_ currently possible to provide `passthrough` as a keyword argument to `responses.add` nor the [shortcut methods](https://github.com/getsentry/responses/tree/3d83a88382bf5cedefbbab3986bbfeeb07883fac#shortcuts).

### Illustration

Providing that keyword argument on `BaseResponse` would allow simplification of code that looks like this:

```py
with responses.RequestsMock() as rsps:
    for url, content in mock_requests:
        rsp = responses.Response(responses.GET, url, body=content)  # note: temporary local variable declared to hold a response
        rsp.passthrough = running_online
        rsps.add(rsp)
```

...to code that looks like this:

```py
with responses.RequestsMock() as rsps:
    for url, content in mock_requests:
        rsps.get(url=url, body=content, passthrough=running_online)
```

Not a huge readability saving, perhaps - although I think it may be relatively straightforward to support, and I haven't yet thought of any downsides (but perhaps it isn't, for some intentional reason? - constructor argument pollution?).  I'll offer a pull request with a potential implementation.

### Related

Extends #399.

Discussion:
jayaddison: > also, do we need then class attribute ?

Good question.  I've left it as-is for now for backwards-compatibility, although I have replaced the mention of it in the documentation.

Perhaps it could make sense to remove it.  Pattern-wise, perhaps we'd want to recommend that `Response` objects should be mostly/completely immutable after they are created?
codecov[bot]: # [Codecov](https://app.codecov.io/gh/getsentry/responses/compare/558?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getsentry) Report
> Merging [#558](https://app.codecov.io/gh/getsentry/responses/compare/558?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getsentry) (07e628a) into [master](https://codecov.io/gh/getsentry/responses/commit/3d83a88382bf5cedefbbab3986bbfeeb07883fac?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getsentry) (3d83a88) will **not change** coverage.
> The diff coverage is `100.00%`.

```diff
@@            Coverage Diff            @@
##            master      #558   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files            7         7           
  Lines         2561      2581   +20     
=========================================
+ Hits          2561      2581   +20     
```


| [Impacted Files](https://app.codecov.io/gh/getsentry/responses/compare/558?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/558/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/tests/test\_responses.py](https://app.codecov.io/gh/getsentry/responses/compare/558/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/558?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/558?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getsentry). Last update [3d83a88...07e628a](https://app.codecov.io/gh/getsentry/responses/compare/558?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 fbf8ddb..bcd26b5 100644
--- a/CHANGES
+++ b/CHANGES
@@ -1,3 +1,8 @@
+0.22.0
+------
+
+* Add `passthrough` argument to `BaseResponse` object. See #557
+
 0.21.0
 ------
 
diff --git a/README.rst b/README.rst
index cd06dba..357bd76 100644
--- a/README.rst
+++ b/README.rst
@@ -1172,14 +1172,18 @@ need to allow an entire domain or path subtree to send requests:
     responses.add_passthru(re.compile("https://percy.io/\\w+"))
 
 
-Lastly, you can use the ``response.passthrough`` attribute on ``BaseResponse`` or
-use ``PassthroughResponse`` to enable a response to behave as a pass through.
+Lastly, you can use the ``passthrough`` argument of the ``Response`` object
+to force a response to behave as a pass through.
 
 .. code-block:: python
 
     # Enable passthrough for a single response
-    response = Response(responses.GET, "http://example.com", body="not used")
-    response.passthrough = True
+    response = Response(
+        responses.GET,
+        "http://example.com",
+        body="not used",
+        passthrough=True,
+    )
     responses.add(response)
 
     # Use PassthroughResponse
diff --git a/responses/__init__.py b/responses/__init__.py
index 9ab2570..30b9597 100644
--- a/responses/__init__.py
+++ b/responses/__init__.py
@@ -354,6 +354,8 @@ class BaseResponse(object):
         url: _URLPatternType,
         match_querystring: Union[bool, object] = None,
         match: "_MatcherIterable" = (),
+        *,
+        passthrough: bool = False,
     ) -> None:
         self.method: str = method
         # ensure the url has a default path set if the url is a string
@@ -366,6 +368,7 @@ class BaseResponse(object):
 
         self.match: "_MatcherIterable" = match
         self.call_count: int = 0
+        self.passthrough = passthrough
 
     def __eq__(self, other: Any) -> bool:
         if not isinstance(other, BaseResponse):
@@ -596,7 +599,8 @@ class CallbackResponse(BaseResponse):
 
 
 class PassthroughResponse(BaseResponse):
-    passthrough: bool = True
+    def __init__(self, *args: Any, **kwargs: Any):
+        super().__init__(*args, passthrough=True, **kwargs)
 
 
 class OriginalResponseShim(object):


Test patch:
diff --git a/responses/tests/test_responses.py b/responses/tests/test_responses.py
index 4dd93f0..083fced 100644
--- a/responses/tests/test_responses.py
+++ b/responses/tests/test_responses.py
@@ -1589,6 +1589,31 @@ def test_passthrough_flag(httpserver):
     assert_reset()
 
 
+def test_passthrough_kwarg(httpserver):
+    httpserver.serve_content("OK", headers={"Content-Type": "text/plain"})
+
+    def configure_response(passthrough):
+        responses.get(httpserver.url, body="MOCK", passthrough=passthrough)
+
+    @responses.activate
+    def run_passthrough():
+        configure_response(passthrough=True)
+        resp = requests.get(httpserver.url)
+        assert_response(resp, "OK")
+
+    @responses.activate
+    def run_mocked():
+        configure_response(passthrough=False)
+        resp = requests.get(httpserver.url)
+        assert_response(resp, "MOCK")
+
+    run_mocked()
+    assert_reset()
+
+    run_passthrough()
+    assert_reset()
+
+
 def test_passthrough_response(httpserver):
     httpserver.serve_content("OK", headers={"Content-Type": "text/plain"})