Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor call stats with easier api #85

Merged
merged 8 commits into from
Oct 9, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions respx/api.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
from typing import Callable, Optional, Pattern, Union, overload

from .mocks import MockTransport
from .models import ContentDataTypes, DefaultType, HeaderTypes, RequestPattern
from .models import CallList, ContentDataTypes, DefaultType, HeaderTypes, RequestPattern

mock = MockTransport(assert_all_called=False)

aliases = mock.aliases
stats = mock.stats
calls = mock.calls
calls: CallList = mock.calls


def start() -> None:
Expand Down
35 changes: 29 additions & 6 deletions respx/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,15 @@
import re
from functools import partial
from typing import (
TYPE_CHECKING,
Any,
AsyncIterable,
Callable,
Dict,
Generator,
Iterable,
List,
NamedTuple,
Optional,
Pattern,
Sequence,
Expand All @@ -21,6 +24,10 @@
import httpx
from httpcore import AsyncByteStream, SyncByteStream

if TYPE_CHECKING:
from unittest.mock import _CallList # pragma: nocover


URL = Tuple[bytes, bytes, Optional[int], bytes]
Headers = List[Tuple[bytes, bytes]]
Request = Tuple[
Expand Down Expand Up @@ -91,6 +98,24 @@ def decode_response(
)


class Call(NamedTuple):
request: httpx.Request
response: Optional[httpx.Response]


class CallList(list):
def __iter__(self) -> Generator[Call, None, None]:
yield from super().__iter__()

@classmethod
def from_unittest_call_list(cls, call_list: "_CallList") -> "CallList":
return cls(Call(request, response) for (request, response), _ in call_list)

@property
def last(self) -> Optional[Call]:
return self[-1] if self else None


class ResponseTemplate:
_content: Optional[ContentDataTypes]
_text: Optional[str]
Expand Down Expand Up @@ -284,18 +309,16 @@ def __init__(
self.stats = mock.MagicMock()

@property
def called(self):
def called(self) -> bool:
return self.stats.called

@property
def call_count(self):
def call_count(self) -> int:
return self.stats.call_count

@property
def calls(self):
return [
(request, response) for (request, response), _ in self.stats.call_args_list
]
def calls(self) -> CallList:
return CallList.from_unittest_call_list(self.stats.call_args_list)

def get_url(self) -> Optional[URLPatternTypes]:
return self._url
Expand Down
8 changes: 3 additions & 5 deletions respx/transports.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,13 @@
from .models import (
URL,
AsyncResponse,
CallList,
ContentDataTypes,
DefaultType,
Headers,
HeaderTypes,
Request,
RequestPattern,
Response,
ResponseTemplate,
SyncResponse,
decode_request,
Expand All @@ -40,7 +40,7 @@ def __init__(
self.aliases: Dict[str, RequestPattern] = {}

self.stats = mock.MagicMock()
self.calls: List[Tuple[Request, Optional[Response]]] = []
self.calls: CallList = CallList()

def clear(self):
"""
Expand Down Expand Up @@ -293,9 +293,7 @@ def record(
self.stats(request, response)

# Copy stats due to unwanted use of property refs in the high-level api
self.calls[:] = (
(request, response) for (request, response), _ in self.stats.call_args_list
)
self.calls[:] = CallList.from_unittest_call_list(self.stats.call_args_list)

def assert_all_called(self) -> None:
assert all(
Expand Down
4 changes: 3 additions & 1 deletion tests/test_mock.py
Original file line number Diff line number Diff line change
Expand Up @@ -226,9 +226,11 @@ async def test(respx_mock):
assert respx.stats.call_count == 0
assert respx_mock.stats.call_count == 1

_request, _response = respx_mock.calls[-1]
_request, _response = respx_mock.calls.last
assert _request is not None
assert _response is not None
assert respx_mock.calls.last.request is _request
assert respx_mock.calls.last.response is _response
assert _request.url == "https://some.thing/"

await test()
Expand Down
5 changes: 4 additions & 1 deletion tests/test_stats.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ async def test(backend):
assert foobar1.called is False
assert foobar1.call_count == len(foobar1.calls)
assert foobar1.call_count == 0
assert foobar1.calls.last is None
assert respx.stats.call_count == len(respx.calls)
assert respx.stats.call_count == 0

Expand All @@ -51,6 +52,8 @@ async def test(backend):
_request, _response = foobar1.calls[-1]
assert isinstance(_request, httpx.Request)
assert isinstance(_response, httpx.Response)
assert foobar1.calls.last.request is _request
assert foobar1.calls.last.response is _response
assert _request.method == "GET"
assert _request.url == url
assert _response.status_code == get_response.status_code == 202
Expand Down Expand Up @@ -82,8 +85,8 @@ async def test(backend):
if isinstance(backend, TrioBackend):
trio.run(test, backend)
else:
loop = asyncio.new_event_loop()
try:
loop = asyncio.new_event_loop()
loop.run_until_complete(test(backend))
finally:
loop.close()