Repository: getsentry/responses

Issue:
Exception variable does not include request object
### Describe the bug

When handling an exception in the requests library, the exception variable stores the `request` object, which can be very helpful in grabbing details about the failed request:

```
try:
    requests.get(<URL>)
    return "Request succeeded"
except Exception as exc:
    request = exc.request  # <- requests.models.PreparedRequest
    return f"Request to url {request.url} with body {request.body} failed."
```

I want to use the responses library to test code like this but `exc.request` just returns `None`.

### Additional context

_No response_

### Version of `responses`

0.21.0

### Steps to Reproduce

The following snippet shows the requests library behavior.

```python
import requests

def make_request(verb):
    try:
        requests.request(verb, "https://httpbin.org/delay/2", timeout=1)
    except requests.ReadTimeout as exc:
        return type(exc.request)

assert make_request("GET") == requests.models.PreparedRequest
assert make_request("POST") == requests.models.PreparedRequest
```

The following tests show the different behavior in the `responses` library. Both tests fail because `make_request(verb)` is returning `None` instead of `requests.models.PreparedRequest`.

```python
import requests
import responses

@responses.activate
def test_make_get_request():
    responses.get("https://httpbin.org/delay/2", body=requests.ReadTimeout())
    assert make_request("GET") == requests.models.PreparedRequest

@responses.activate
def test_make_post_request():
    responses.post("https://httpbin.org/delay/2", body=requests.ReadTimeout())
    assert make_request("POST") == requests.models.PreparedRequest
```

### Expected Result

Both unit tests in the "Steps to Reproduce" section fail. They should pass.

### Actual Result

Looks like the `request` object is not being passed to the Exception [here](https://github.com/getsentry/responses/blob/6144ef5dcc84389e395592e54628ebb0a42e34de/responses/__init__.py#L530)? I'm happy to work on a PR if you think this is a bug.

Comments:

Patch:
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 13e14df..f8566ec 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -20,7 +20,7 @@ jobs:
       - run: |
           pip install wheel
           python setup.py bdist_wheel sdist --formats=gztar
-      - uses: actions/upload-artifact@v2
+      - uses: actions/upload-artifact@v3.1.1
         with:
           name: ${{ github.sha }}
           path: dist/*
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 236a25d..f638173 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -21,7 +21,7 @@ jobs:
 
     - uses: actions/setup-python@v4
       with:
-        python-version: 3.8
+        python-version: '3.10'
 
     - name: Install dependencies
       run: |
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 0fb53d7..20e0535 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -1,11 +1,11 @@
 repos:
   - repo: https://github.com/psf/black
-    rev: 22.3.0
+    rev: 22.12.0
     hooks:
     - id: black
       args: [--line-length=88, --safe]
   - repo: https://github.com/pre-commit/pre-commit-hooks
-    rev: v4.1.0
+    rev: v4.4.0
     hooks:
     - id: check-case-conflict
     - id: check-merge-conflict
@@ -15,13 +15,13 @@ repos:
     - id: debug-statements
     - id: requirements-txt-fixer
   - repo: https://github.com/pycqa/isort
-    rev: 5.10.1
+    rev: 5.11.4
     hooks:
       - id: isort
         name: isort (python)
         args: ['--force-single-line-imports', '--profile', 'black']
   - repo: https://github.com/pycqa/flake8
-    rev: 4.0.1
+    rev: 6.0.0
     hooks:
       - id: flake8
         args: [ '--max-line-length', '100', '--max-doc-length', '120' ]
diff --git a/CHANGES b/CHANGES
index fa3e8cc..5542677 100644
--- a/CHANGES
+++ b/CHANGES
@@ -2,8 +2,9 @@
 ------
 
 * Add Python 3.11 support
-* Fix type annotations of `CallList`.
+* Fix type annotations of `CallList`. See #593
 * Replaced toml with tomli and tomli-w.
+* `request` object is attached to any custom exception provided as `Response` `body` argument. See #588
 
 0.22.0
 ------
diff --git a/README.rst b/README.rst
index 6610e7e..0e4eec5 100644
--- a/README.rst
+++ b/README.rst
@@ -505,6 +505,8 @@ Request Keyword Arguments Matcher
 To validate request arguments use the ``matchers.request_kwargs_matcher`` function to match
 against the request kwargs.
 
+Only following arguments are supported: ``timeout``, ``verify``, ``proxies``, ``stream``, ``cert``.
+
 Note, only arguments provided to ``matchers.request_kwargs_matcher`` will be validated.
 
 .. code-block:: python
diff --git a/responses/__init__.py b/responses/__init__.py
index 0969312..514392b 100644
--- a/responses/__init__.py
+++ b/responses/__init__.py
@@ -569,6 +569,7 @@ class Response(BaseResponse):
 
     def get_response(self, request: "PreparedRequest") -> HTTPResponse:
         if self.body and isinstance(self.body, Exception):
+            setattr(self.body, "request", request)
             raise self.body
 
         headers = self.get_headers()


Test patch:
diff --git a/responses/tests/test_responses.py b/responses/tests/test_responses.py
index 53924df..e8197a3 100644
--- a/responses/tests/test_responses.py
+++ b/responses/tests/test_responses.py
@@ -2494,3 +2494,20 @@ class TestMaxRetry:
 
         run()
         assert_reset()
+
+
+def test_request_object_attached_to_exception():
+    """Validate that we attach `request` object to custom exception supplied as body"""
+
+    @responses.activate
+    def run():
+        url = "https://httpbin.org/delay/2"
+        responses.get(url, body=requests.ReadTimeout())
+
+        try:
+            requests.get(url, timeout=1)
+        except requests.ReadTimeout as exc:
+            assert type(exc.request) == requests.models.PreparedRequest
+
+    run()
+    assert_reset()