Repository: getsentry/responses

Issue:
Facilitate how to view the registered responses for debugging purposes
<!--
Please provide some context on what problem you are trying to solve and how this feature is going to help.

-->
Hi maintainers! This is my first issue and intent to help here 😄 

## Motivation
When writing my tests and debugging them, sometimes I want to know which are the registered responses so far since we tend to use different fixtures to add responses and we lose track of which are the URLs expected to be called before the test ends.

The documentation says that in order to get some access to them we should access them via `responses.mock._matches` which in turn, it will return the list of `Response` objects and we have to access them to have information such as the precious URL, as in:
```
[
    <responses.Response object at 0x10a121670>,
    <responses.Response object at 0x10a1216a0>,
    <responses.Response object at 0x10a1216d0>
]
```

## Suggestion
To have a method that facilitates retrieving information of which are these registered responses.

I'd suggest something like `responses.mock.show_registered(urls_only: bool = False)` and return a list of the URLs registered if `urls_only == True` or a dictionary structure nested by `method` and `url`.
For example having:
```
responses.add(
    responses.GET, "http://example.com/", body="", headers={"X-Test": "foo"}
)
responses.add(
    responses.GET, "http://example.com/", body="", headers={"X-Test": "foo"}
)
responses.add(
    responses.POST, "http://anotherdomain.com/", body={"field": "value"}
)
```
then calling `responses.mock.show_registered(urls_only=True)` would return:
```
["http://example.com/", "http://example.com/", "http://anotherdomain.com/" ]
```
or by calling just `responses.mock.show_registered()` would return:
```
{
    'GET': {
        'http://example.com/': [
            {
                'body': '',
                'content_type': 'text/plain',
                'headers': {'X-Test': 'foo'},
                'status': 200
            },
            {
                'body': '',
                'content_type': 'text/plain',
                'headers': {'X-Test': 'foo'},
                'status': 200
            }
        ]
    },
    'POST': {
        'http://anotherdomain.com/': [
            {
                'body': {'field': 'value'},
                'content_type': 'text/plain',
                'headers': None,
                'status': 200
            }
        ]
    }
}
```

## Implementation
I have already implemented this in a local branch, if you think it could be useful I can create a Pull Request for you to review it and give me some feedback.



Discussion:
codecov[bot]: # [Codecov](https://app.codecov.io/gh/getsentry/responses/compare/370?src=pr&el=h1) Report
> Merging [#370](https://app.codecov.io/gh/getsentry/responses/compare/370?src=pr&el=desc) (e1f2217) into [master](https://codecov.io/gh/getsentry/responses/commit/736fd0e07965b4d985eb29bd1e04c0ac90b97a22?el=desc) (736fd0e) will **decrease** coverage by `1.79%`.
> The diff coverage is `100.00%`.

[![Impacted file tree graph](https://codecov.io/gh/getsentry/responses/pull/370/graphs/tree.svg?width=650&height=150&src=pr&token=x2tAGR7rxE)](https://app.codecov.io/gh/getsentry/responses/compare/370?src=pr&el=tree)

```diff
@@            Coverage Diff             @@
##           master     #370      +/-   ##
==========================================
- Coverage   96.51%   94.71%   -1.80%     
==========================================
  Files           3        3              
  Lines        1319     1344      +25     
==========================================
  Hits         1273     1273              
- Misses         46       71      +25     
```


| [Impacted Files](https://app.codecov.io/gh/getsentry/responses/compare/370?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [responses/\_\_init\_\_.py](https://app.codecov.io/gh/getsentry/responses/compare/370/diff?src=pr&el=tree#diff-cmVzcG9uc2VzL19faW5pdF9fLnB5) | `92.60% <100.00%> (-5.13%)` | :arrow_down: |
| [responses/test\_responses.py](https://app.codecov.io/gh/getsentry/responses/compare/370/diff?src=pr&el=tree#diff-cmVzcG9uc2VzL3Rlc3RfcmVzcG9uc2VzLnB5) | `97.94% <100.00%> (-0.19%)` | :arrow_down: |

------

[Continue to review full report at Codecov](https://app.codecov.io/gh/getsentry/responses/compare/370?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://app.codecov.io/gh/getsentry/responses/compare/370?src=pr&el=footer). Last update [736fd0e...e1f2217](https://app.codecov.io/gh/getsentry/responses/compare/370?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).

mromagnoli: I have to leave for today, I'll check tomorrow all the failing checks 👍 
mromagnoli: In order to avoid the mypy error that is happening in all other PRs and master, we have to add `upsert` to the list of stubs in `__init__.pyi`
mromagnoli: To update, there `__repr__` and `__str__` are returning the same since just `__repr__` is implemented.
The format is:
```
"<Response(url='http://example.com/' status=200 content_type='text/plain' headers='null')>"
```
and `responses.mock.registered()` lists responses as:
```
ipdb> responses.mock.registered()

[<Response(url='http://example.com/' status=404 content_type='text/plain' headers='{"X-Test": "foo"}')>,
<Response(url='http://example.com/' status=200 content_type='text/plain' headers='{"X-Test": "foo"}')>, 
<Response(url='http://anotherdomain.com/' status=200 content_type='text/plain' headers='null')>]
```
mromagnoli: Hi @markstory  👋  how do you feel about this last version? Do you think we are good to go?

Patch:
diff --git a/README.rst b/README.rst
index 1bfa062..20f5f27 100644
--- a/README.rst
+++ b/README.rst
@@ -475,9 +475,9 @@ Regex can be used like:
 Viewing/Modifying registered responses
 --------------------------------------
 
-Registered responses are available as a private attribute of the RequestMock
+Registered responses are available as a public method of the RequestMock
 instance. It is sometimes useful for debugging purposes to view the stack of
-registered responses which can be accessed via ``responses.mock._matches``.
+registered responses which can be accessed via ``responses.registered()``.
 
 The ``replace`` function allows a previously registered ``response`` to be
 changed. The method signature is identical to ``add``. ``response`` s are
diff --git a/responses/__init__.py b/responses/__init__.py
index 7e4a6e5..3eb9399 100644
--- a/responses/__init__.py
+++ b/responses/__init__.py
@@ -418,6 +418,17 @@ class Response(BaseResponse):
             preload_content=False,
         )
 
+    def __repr__(self):
+        return (
+            "<Response(url='{url}' status={status} "
+            "content_type='{content_type}' headers='{headers}')>".format(
+                url=self.url,
+                status=self.status,
+                content_type=self.content_type,
+                headers=json_module.dumps(self.headers),
+            )
+        )
+
 
 class CallbackResponse(BaseResponse):
     def __init__(
@@ -651,6 +662,9 @@ class RequestsMock(object):
             )
         )
 
+    def registered(self):
+        return self._matches
+
     @property
     def calls(self):
         return self._calls
@@ -809,6 +823,7 @@ __all__ = [
     "PATCH",
     "POST",
     "PUT",
+    "registered",
     "remove",
     "replace",
     "reset",
@@ -834,6 +849,7 @@ passthru_prefixes = _default_mock.passthru_prefixes
 PATCH = _default_mock.PATCH
 POST = _default_mock.POST
 PUT = _default_mock.PUT
+registered = _default_mock.registered
 remove = _default_mock.remove
 replace = _default_mock.replace
 reset = _default_mock.reset
diff --git a/responses/__init__.pyi b/responses/__init__.pyi
index 92ad071..b437a34 100644
--- a/responses/__init__.pyi
+++ b/responses/__init__.pyi
@@ -175,6 +175,7 @@ class RequestsMock:
     def start(self) -> None: ...
     def stop(self, allow_assert: bool = ...) -> None: ...
     def assert_call_count(self, url: str, count: int) -> bool: ...
+    def registered(self) -> List[Any]: ...
 
 _F = TypeVar("_F", bound=Callable[..., Any])
 
@@ -251,6 +252,10 @@ class _Upsert(Protocol):
         match: List[Any] = ...,
     ) -> None: ...
 
+class _Registered(Protocol):
+    def __call__(self) -> List[Response]: ...
+
+
 activate: _Activate
 add: _Add
 add_callback: _AddCallback
@@ -268,6 +273,7 @@ passthru_prefixes: Tuple[str, ...]
 PATCH: Literal["PATCH"]
 POST: Literal["POST"]
 PUT: Literal["PUT"]
+registered: _Registered
 remove: _Remove
 replace: _Replace
 reset: Callable[[], None]
@@ -297,6 +303,7 @@ __all__ = [
     "PATCH",
     "POST",
     "PUT",
+    "registered",
     "remove",
     "replace",
     "reset",


Test patch:
diff --git a/responses/test_responses.py b/responses/test_responses.py
index fdab8da..6ab8606 100644
--- a/responses/test_responses.py
+++ b/responses/test_responses.py
@@ -1397,3 +1397,78 @@ def test_fail_request_error():
 
     run()
     assert_reset()
+
+
+@pytest.mark.parametrize(
+    "response_params, expected_representation",
+    [
+        (
+            {"method": responses.GET, "url": "http://example.com/"},
+            (
+                "<Response(url='http://example.com/' status=200 "
+                "content_type='text/plain' headers='null')>"
+            ),
+        ),
+        (
+            {
+                "method": responses.POST,
+                "url": "http://another-domain.com/",
+                "content_type": "application/json",
+                "status": 404,
+            },
+            (
+                "<Response(url='http://another-domain.com/' status=404 "
+                "content_type='application/json' headers='null')>"
+            ),
+        ),
+        (
+            {
+                "method": responses.PUT,
+                "url": "http://abcd.com/",
+                "content_type": "text/html",
+                "status": 500,
+                "headers": {"X-Test": "foo"},
+                "body": {"it_wont_be": "considered"},
+            },
+            (
+                "<Response(url='http://abcd.com/' status=500 "
+                "content_type='text/html' headers='{\"X-Test\": \"foo\"}')>"
+            ),
+        ),
+    ],
+)
+def test_response_representations(response_params, expected_representation):
+    response = Response(**response_params)
+
+    assert str(response) == expected_representation
+    assert repr(response) == expected_representation
+
+
+def test_mocked_responses_list_registered():
+    @responses.activate
+    def run():
+        first_response = Response(
+            responses.GET,
+            "http://example.com/",
+            body="",
+            headers={"X-Test": "foo"},
+            status=404,
+        )
+        second_response = Response(
+            responses.GET, "http://example.com/", body="", headers={"X-Test": "foo"}
+        )
+        third_response = Response(
+            responses.POST,
+            "http://anotherdomain.com/",
+        )
+        responses.add(first_response)
+        responses.add(second_response)
+        responses.add(third_response)
+
+        mocks_list = responses.registered()
+
+        assert mocks_list == responses.mock._matches
+        assert mocks_list == [first_response, second_response, third_response]
+
+    run()
+    assert_reset()