Repository: getsentry/responses

Issue:
Add shortcuts for each HTTP verb
When using a pytest fixture like this:

```py
@pytest.fixture
def mocked_responses():
    with responses.RequestsMock() as rsps:
        yield rsps
```

In my tests, I have to import `responses` to add mocks:

```py
import responses

def test_server(mocked_responses):
     mocked_responses.add(responses.GET, "http://server/fake", json={})
```

Instead, I believe it would be more convenient to have a shortcut for each verb:

```py
def test_server(mocked_responses):
     mocked_responses.get("http://server/fake", json={})
```

> Note: this is the API offered by [aioresponses](https://github.com/pnuckowski/aioresponses/)

The contribution is trivial, and I would be happy to do it. But first, what do you think?

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

[![Impacted file tree graph](https://codecov.io/gh/getsentry/responses/pull/508/graphs/tree.svg?width=650&height=150&src=pr&token=x2tAGR7rxE&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getsentry)](https://app.codecov.io/gh/getsentry/responses/compare/508?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getsentry)

```diff
@@            Coverage Diff            @@
##            master      #508   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files            6         6           
  Lines         2200      2278   +78     
=========================================
+ Hits          2200      2278   +78     
```


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

------

[Continue to review full report at Codecov](https://app.codecov.io/gh/getsentry/responses/compare/508?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/508?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getsentry). Last update [a35896e...350c5d1](https://app.codecov.io/gh/getsentry/responses/compare/508?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 6f3cc30..e901fd2 100644
--- a/CHANGES
+++ b/CHANGES
@@ -3,6 +3,9 @@
 
 * Added a registry that provides more strict ordering based on the invocation index.
   See `responses.registries.OrderedRegistry`.
+* Added shortcuts for each request method: delete, get, head, options, patch, post, put.
+  For example, to add response for POST request you can use `responses.post()` instead
+  of `responses.add(responses.POST)`.
 * Prevent `responses.activate` decorator to leak, if wrapped function called from within another
   wrapped function. Also, allow calling of above mentioned chain. See #481 for more details.
 * Expose `get_registry()` method of `RequestsMock` object. Replaces internal `_get_registry()`.
@@ -10,7 +13,11 @@
 * Add support for the `loose` version of `query_param_matcher` via named argument `strict_match`.
 * Added support for `async/await` functions.
 * `response_callback` is no longer executed on exceptions raised by failed `Response`s
+* Change logic of `_get_url_and_path` to comply with RFC 3986. Now URL match occurs by matching
+  schema, authority and path, where path is terminated by the first question mark ("?") or
+  number sign ("#") character, or by the end of the URI.
 * An error is now raised when both `content_type` and `headers[content-type]` are provided as parameters.
+* When a request isn't matched the passthru prefixes are now included in error messages.
 
 
 0.18.0
diff --git a/responses/__init__.py b/responses/__init__.py
index 70c6260..2f512b2 100644
--- a/responses/__init__.py
+++ b/responses/__init__.py
@@ -38,7 +38,6 @@ from io import BytesIO
 from unittest import mock as std_mock
 from urllib.parse import parse_qsl
 from urllib.parse import quote
-from urllib.parse import urlparse
 from urllib.parse import urlsplit
 from urllib.parse import urlunparse
 from urllib.parse import urlunsplit
@@ -213,7 +212,7 @@ def _ensure_url_default_path(url):
 
 
 def _get_url_and_path(url):
-    url_parsed = urlparse(url)
+    url_parsed = urlsplit(url)
     url_and_path = urlunparse(
         [url_parsed.scheme, url_parsed.netloc, url_parsed.path, None, None, None]
     )
@@ -269,7 +268,7 @@ class BaseResponse(object):
         self.url = _ensure_url_default_path(url)
 
         if self._should_match_querystring(match_querystring):
-            match = tuple(match) + (_query_string_matcher(urlparse(self.url).query),)
+            match = tuple(match) + (_query_string_matcher(urlsplit(self.url).query),)
 
         self.match = match
         self.call_count = 0
@@ -309,7 +308,7 @@ class BaseResponse(object):
                 )
             return match_querystring_argument
 
-        return bool(urlparse(self.url).query)
+        return bool(urlsplit(self.url).query)
 
     def _url_matches(self, url, other):
         if isinstance(url, str):
@@ -616,6 +615,27 @@ class RequestsMock(object):
 
         self._registry.add(Response(method=method, url=url, body=body, **kwargs))
 
+    def delete(self, *args, **kwargs):
+        self.add(DELETE, *args, **kwargs)
+
+    def get(self, *args, **kwargs):
+        self.add(GET, *args, **kwargs)
+
+    def head(self, *args, **kwargs):
+        self.add(HEAD, *args, **kwargs)
+
+    def options(self, *args, **kwargs):
+        self.add(OPTIONS, *args, **kwargs)
+
+    def patch(self, *args, **kwargs):
+        self.add(PATCH, *args, **kwargs)
+
+    def post(self, *args, **kwargs):
+        self.add(POST, *args, **kwargs)
+
+    def put(self, *args, **kwargs):
+        self.add(PUT, *args, **kwargs)
+
     def add_passthru(self, prefix):
         """
         Register a URL prefix or regex to passthru any non-matching mock requests to.
@@ -744,7 +764,7 @@ class RequestsMock(object):
 
     def _parse_request_params(self, url):
         params = {}
-        for key, val in groupby(parse_qsl(urlparse(url).query), lambda kv: kv[0]):
+        for key, val in groupby(parse_qsl(urlsplit(url).query), lambda kv: kv[0]):
             values = list(map(lambda x: x[1], val))
             if len(values) == 1:
                 values = values[0]
@@ -784,6 +804,11 @@ class RequestsMock(object):
                     m.method, m.url, match_failed_reasons[i]
                 )
 
+            if self.passthru_prefixes:
+                error_msg += "Passthru prefixes:\n"
+                for p in self.passthru_prefixes:
+                    error_msg += "- {}\n".format(p)
+
             response = ConnectionError(error_msg)
             response.request = request
 
@@ -866,13 +891,20 @@ __all__ = [
     "assert_all_requests_are_fired",
     "assert_call_count",
     "calls",
+    "delete",
     "DELETE",
+    "get",
     "GET",
+    "head",
     "HEAD",
+    "options",
     "OPTIONS",
     "passthru_prefixes",
+    "patch",
     "PATCH",
+    "post",
     "POST",
+    "put",
     "PUT",
     "registered",
     "remove",
@@ -892,13 +924,20 @@ add_passthru = _default_mock.add_passthru
 assert_all_requests_are_fired = _default_mock.assert_all_requests_are_fired
 assert_call_count = _default_mock.assert_call_count
 calls = _default_mock.calls
+delete = _default_mock.delete
 DELETE = _default_mock.DELETE
+get = _default_mock.get
 GET = _default_mock.GET
+head = _default_mock.head
 HEAD = _default_mock.HEAD
+options = _default_mock.options
 OPTIONS = _default_mock.OPTIONS
 passthru_prefixes = _default_mock.passthru_prefixes
+patch = _default_mock.patch
 PATCH = _default_mock.PATCH
+post = _default_mock.post
 POST = _default_mock.POST
+put = _default_mock.put
 PUT = _default_mock.PUT
 registered = _default_mock.registered
 remove = _default_mock.remove
diff --git a/responses/__init__.pyi b/responses/__init__.pyi
index 8707f34..273e203 100644
--- a/responses/__init__.pyi
+++ b/responses/__init__.pyi
@@ -185,6 +185,13 @@ class RequestsMock:
         ...
     def reset(self) -> None: ...
     add: _Add
+    delete: _Shortcut
+    get: _Shortcut
+    head: _Shortcut
+    options: _Shortcut
+    patch: _Shortcut
+    post: _Shortcut
+    put: _Shortcut
     add_passthru: _AddPassthru
     def remove(
         self,
@@ -226,6 +233,22 @@ class _Add(Protocol):
         match: MatcherIterable = ...,
     ) -> None: ...
 
+class _Shortcut(Protocol):
+    def __call__(
+        self,
+        url: Optional[Union[Pattern[str], str]] = ...,
+        body: _Body = ...,
+        json: Optional[Any] = ...,
+        status: int = ...,
+        headers: HeaderSet = ...,
+        stream: bool = ...,
+        content_type: Optional[str] = ...,
+        auto_calculate_content_length: bool = ...,
+        adding_headers: HeaderSet = ...,
+        match_querystring: bool = ...,
+        match: MatcherIterable = ...,
+    ) -> None: ...
+
 class _AddCallback(Protocol):
     def __call__(
         self,
@@ -304,15 +327,22 @@ add_passthru: _AddPassthru
 assert_all_requests_are_fired: bool
 assert_call_count: Callable[[str, int], bool]
 calls: CallList
+delete: _Shortcut
 DELETE: Literal["DELETE"]
+get: _Shortcut
 GET: Literal["GET"]
+head: _Shortcut
 HEAD: Literal["HEAD"]
 mock: RequestsMock
 _default_mock: RequestsMock
+options: _Shortcut
 OPTIONS: Literal["OPTIONS"]
 passthru_prefixes: Tuple[str, ...]
+patch: _Shortcut
 PATCH: Literal["PATCH"]
+post: _Shortcut
 POST: Literal["POST"]
+put: _Shortcut
 PUT: Literal["PUT"]
 registered: _Registered
 remove: _Remove
@@ -336,13 +366,20 @@ __all__ = [
     "assert_all_requests_are_fired",
     "assert_call_count",
     "calls",
+    "delete",
     "DELETE",
+    "get",
     "GET",
+    "head",
     "HEAD",
+    "options",
     "OPTIONS",
     "passthru_prefixes",
+    "patch",
     "PATCH",
+    "post",
     "POST",
+    "put",
     "PUT",
     "registered",
     "remove",


Test patch:
diff --git a/responses/test_responses.py b/responses/test_responses.py
index 3ddca9b..80456d2 100644
--- a/responses/test_responses.py
+++ b/responses/test_responses.py
@@ -1158,6 +1158,28 @@ def test_allow_redirects_samehost():
     assert_reset()
 
 
+def test_path_segments():
+    """Test that path segment after ``;`` is preserved.
+
+    Validate compliance with RFC 3986.
+    The path is terminated by the first question mark ("?") or
+    number sign ("#") character, or by the end of the URI.
+    See more about how path should be treated under:
+    https://datatracker.ietf.org/doc/html/rfc3986.html#section-3.3
+    """
+
+    @responses.activate
+    def run():
+        responses.add(responses.GET, "http://example.com/here/we", status=669)
+        responses.add(responses.GET, "http://example.com/here/we;go", status=777)
+
+        resp = requests.get("http://example.com/here/we;go")
+        assert resp.status_code == 777
+
+    run()
+    assert_reset()
+
+
 def test_handles_unicode_querystring():
     url = "http://example.com/test?type=2&ie=utf8&query=汉字"
 
@@ -1817,6 +1839,7 @@ def test_fail_request_error():
         with responses.RequestsMock(assert_all_requests_are_fired=False) as rsps:
             rsps.add("POST", "http://example1.com")
             rsps.add("GET", "http://example.com")
+            rsps.add_passthru("http://other.example.com")
 
             with pytest.raises(ConnectionError) as excinfo:
                 requests.post("http://example.com", data={"id": "bad"})
@@ -1824,6 +1847,7 @@ def test_fail_request_error():
             msg = str(excinfo.value)
             assert "- POST http://example1.com/ URL does not match" in msg
             assert "- GET http://example.com/ Method does not match" in msg
+            assert "Passthru prefixes:\n- http://other.example.com" in msg
 
     run()
     assert_reset()
@@ -2018,3 +2042,75 @@ class TestMultipleWrappers:
 
         response = requests.get(httpserver.url)
         assert response.status_code == 969
+
+
+class TestShortcuts:
+    def test_delete(self):
+        @responses.activate
+        def run():
+            responses.delete("http://example.com/1", status=888)
+            resp = requests.delete("http://example.com/1")
+            assert resp.status_code == 888
+
+        run()
+        assert_reset()
+
+    def test_get(self):
+        @responses.activate
+        def run():
+            responses.get("http://example.com/1", status=888)
+            resp = requests.get("http://example.com/1")
+            assert resp.status_code == 888
+
+        run()
+        assert_reset()
+
+    def test_head(self):
+        @responses.activate
+        def run():
+            responses.head("http://example.com/1", status=888)
+            resp = requests.head("http://example.com/1")
+            assert resp.status_code == 888
+
+        run()
+        assert_reset()
+
+    def test_options(self):
+        @responses.activate
+        def run():
+            responses.options("http://example.com/1", status=888)
+            resp = requests.options("http://example.com/1")
+            assert resp.status_code == 888
+
+        run()
+        assert_reset()
+
+    def test_patch(self):
+        @responses.activate
+        def run():
+            responses.patch("http://example.com/1", status=888)
+            resp = requests.patch("http://example.com/1")
+            assert resp.status_code == 888
+
+        run()
+        assert_reset()
+
+    def test_post(self):
+        @responses.activate
+        def run():
+            responses.post("http://example.com/1", status=888)
+            resp = requests.post("http://example.com/1")
+            assert resp.status_code == 888
+
+        run()
+        assert_reset()
+
+    def test_put(self):
+        @responses.activate
+        def run():
+            responses.put("http://example.com/1", status=888)
+            resp = requests.put("http://example.com/1")
+            assert resp.status_code == 888
+
+        run()
+        assert_reset()