Repository: lundberg/respx

Issue:
Poping a request pattern by alias
I'm using `respx` in pytest fixtures. I found a common issue that I was not able to solve easily: Updating one request pattern in a collection of mocked requests, provided by global fixture.

Example:

```python
@pytest.fixture
def mock_services() -> Iterator[MockTransport]:
    base_services_mock.post(re.compile(SERVICE_A_URL + "/endpoint_a.*"), content=fake_content(), alias="endpoint a")
    base_services_mock.get(re.compile(SERVICE_A_URL + "/endpoint_b.*"), content=fake_content(), alias="endpoint b")
    # more endpoint mocks ...
    yield base_services_mock

@staticmethod
def test_endpoint_a_only(mock_services: MockTransport):
    request_mock = mock_services.aliases.pop("endpoint a")  # remove previous mock
    mock_services.patterns.remove(request_mock)
    mock_services.post(request_mock.url, status_code=HTTP_500_INTERNAL_SERVER_ERROR)

    # run the test. "endpoint a" has been modified, "endpoint b" remains default.
```

I looked inside the class and the `pop` + `remove` functionality could be easily exposed as `pop`, since the class already mimics `dict` interface.

I suggested this change in #60 

Discussion:

Patch:
diff --git a/docs/api.md b/docs/api.md
index c319320..43d152a 100644
--- a/docs/api.md
+++ b/docs/api.md
@@ -175,6 +175,33 @@ def test_something():
     assert response.json() == {"id": 123}
 ```
 
+### Manipulating Existing Patterns
+
+Clearing all existing patterns:
+
+``` python
+import respx
+
+
+@respx.mock
+def test_something():
+    respx.get("https://foo.bar/baz", status_code=404)
+    respx.clear()  # no patterns will be matched after this call
+```
+
+Removing and optionally re-using an existing pattern by alias:
+
+``` python
+import respx
+
+
+@respx.mock
+def test_something():
+    respx.get("https://foo.bar/", status_code=404, alias="index")
+    request_pattern = respx.pop("index")
+    respx.get(request_pattern.url, status_code=200)
+```
+
 ---
 
 ## Response Content
diff --git a/respx/transports.py b/respx/transports.py
index 574058c..8181e0f 100644
--- a/respx/transports.py
+++ b/respx/transports.py
@@ -1,5 +1,17 @@
 import warnings
-from typing import Any, Callable, Coroutine, Dict, List, Optional, Pattern, Tuple, Union
+from typing import (
+    Any,
+    Callable,
+    Coroutine,
+    Dict,
+    List,
+    Optional,
+    Pattern,
+    Tuple,
+    TypeVar,
+    Union,
+    overload,
+)
 
 import asynctest
 from httpcore import (
@@ -23,6 +35,8 @@ from .models import (
     build_response,
 )
 
+DefaultType = TypeVar("DefaultType", bound=Any)
+
 
 class BaseMockTransport:
     def __init__(
@@ -60,6 +74,31 @@ class BaseMockTransport:
     def __getitem__(self, alias: str) -> Optional[RequestPattern]:
         return self.aliases.get(alias)
 
+    @overload
+    def pop(self, alias: str) -> RequestPattern:
+        ...
+
+    @overload
+    def pop(
+        self, alias: str, default: DefaultType
+    ) -> Union[RequestPattern, DefaultType]:
+        ...
+
+    def pop(self, alias, default=...):
+        """
+        Removes a pattern by alias and returns it.
+
+        Raises KeyError when `default` not provided and alias is not found.
+        """
+        try:
+            request_pattern = self.aliases.pop(alias)
+            self.patterns.remove(request_pattern)
+            return request_pattern
+        except KeyError as ex:
+            if default is ...:
+                raise ex
+            return default
+
     def add(
         self,
         method: Union[str, Callable, RequestPattern],


Test patch:
diff --git a/tests/test_transports.py b/tests/test_transports.py
index e446250..3674503 100644
--- a/tests/test_transports.py
+++ b/tests/test_transports.py
@@ -85,3 +85,26 @@ async def test_httpcore_request():
             body = b"".join([chunk async for chunk in stream])
             await stream.aclose()
             assert body == b"foobar"
+
+
+@pytest.mark.asyncio
+async def test_transport_pop():
+    url = "https://foo.bar/"
+    alias = "get_alias"
+
+    transport = AsyncMockTransport()
+    transport.get(url, status_code=404, alias=alias)
+
+    request_pattern = transport.pop(alias)
+
+    assert request_pattern.response.status_code == 404
+    assert request_pattern.alias == alias
+    assert request_pattern.url == url
+
+    assert not transport.aliases
+    assert not transport.patterns
+
+    with pytest.raises(KeyError):
+        transport.pop(alias)
+
+    assert transport.pop(alias, "custom default") == "custom default"