Skip to content

Commit

Permalink
Merge remote-tracking branch 'pnuckowski/aioresponses/master' into su…
Browse files Browse the repository at this point in the history
…pport-python-3.10-and-aiohttp-3.8
  • Loading branch information
marcinsulikowski committed Feb 18, 2023
2 parents d946dcb + f330e71 commit b401e6c
Show file tree
Hide file tree
Showing 6 changed files with 301 additions and 41 deletions.
67 changes: 44 additions & 23 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ on:
pull_request:
branches: [ master ]

env:
python-major-version: 3

concurrency:
group: ci-${{ github.ref }}-${{ github.actor }}
cancel-in-progress: true
Expand All @@ -14,64 +17,82 @@ jobs:
build:
env:
PYTEST_ADDOPTS: "--cov --cov-report=xml"
runs-on: ubuntu-latest

strategy:
fail-fast: false
matrix:
include:
- python-version: 3.7
- python-minor-version: 7
aiohttp-version: aiohttp33
- python-version: 3.7
os: ubuntu-latest
- python-minor-version: 7
aiohttp-version: aiohttp34
- python-version: 3.7
os: ubuntu-latest
- python-minor-version: 7
aiohttp-version: aiohttp35
- python-version: 3.7
os: ubuntu-latest
- python-minor-version: 7
aiohttp-version: aiohttp36
- python-version: 3.7
os: ubuntu-latest
- python-minor-version: 7
aiohttp-version: aiohttp37
- python-version: 3.7
os: ubuntu-latest
- python-minor-version: 7
aiohttp-version: aiohttp38
os: ubuntu-latest

- python-version: 3.8
- python-minor-version: 8
aiohttp-version: aiohttp33
- python-version: 3.8
os: ubuntu-latest
- python-minor-version: 8
aiohttp-version: aiohttp34
- python-version: 3.8
os: ubuntu-latest
- python-minor-version: 8
aiohttp-version: aiohttp35
- python-version: 3.8
os: ubuntu-latest
- python-minor-version: 8
aiohttp-version: aiohttp36
- python-version: 3.8
os: ubuntu-latest
- python-minor-version: 8
aiohttp-version: aiohttp37
- python-version: 3.8
os: ubuntu-latest
- python-minor-version: 8
aiohttp-version: aiohttp38
os: ubuntu-latest

- python-version: 3.9
- python-minor-version: 9
aiohttp-version: aiohttp35
- python-version: 3.9
os: ubuntu-latest
- python-minor-version: 9
aiohttp-version: aiohttp36
- python-version: 3.9
os: ubuntu-latest
- python-minor-version: 9
aiohttp-version: aiohttp37
- python-version: 3.9
os: ubuntu-latest
- python-minor-version: 9
aiohttp-version: aiohttp38
os: ubuntu-latest

- python-version: "3.10"
- python-minor-version: 10
aiohttp-version: aiohttp37
- python-version: "3.10"
os: ubuntu-latest
- python-minor-version: 10
aiohttp-version: aiohttp38

os: ubuntu-latest
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v2
- name: Set up Python ${{ matrix.python-version }}
- name: Set up Python ${{ env.python-major-version }}.${{ matrix.python-minor-version }}
uses: actions/setup-python@v2
with:
python-version: ${{ matrix.python-version }}
python-version: ${{ env.python-major-version }}.${{ matrix.python-minor-version }}
- name: Install Dependencies
run: |
python -m pip install --upgrade pip
pip install tox
- name: Run Tests
run: |
tox -e py${{ matrix.python-version }}-${{ matrix.aiohttp-version }}
tox -e py${{ env.python-major-version }}${{ matrix.python-version }}-${{ matrix.aiohttp-version }}
- uses: codecov/codecov-action@v2
with:
file: coverage.xml
3 changes: 3 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ Supported HTTP methods: **GET**, **POST**, **PUT**, **PATCH**, **DELETE** and **
resp = loop.run_until_complete(session.get('http://example.com'))
assert resp.status == 200
mocked.assert_called_once_with('http://example.com')
for convenience use *payload* argument to mock out json response. Example below.
Expand All @@ -88,6 +89,7 @@ for convenience use *payload* argument to mock out json response. Example below.
data = loop.run_until_complete(resp.json())
assert dict(foo='bar') == data
m.assert_called_once_with('http://test.example.com')
**aioresponses allows to mock out any HTTP headers**

Expand All @@ -112,6 +114,7 @@ for convenience use *payload* argument to mock out json response. Example below.
# note that we pass 'connection' but get 'Connection' (capitalized)
# under the neath `multidict` is used to work with HTTP headers
assert resp.headers['Connection'] == 'keep-alive'
m.assert_called_once_with('http://example.com', method='POST')
**allows to register different responses for the same url**

Expand Down
153 changes: 139 additions & 14 deletions aioresponses/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,10 @@ def match_str(self, url: URL) -> bool:
return self.url_or_pattern == url

def match_regexp(self, url: URL) -> bool:
return bool(self.url_or_pattern.match(str(url)))
# This method is used if and only if self.url_or_pattern is a pattern.
return bool(
self.url_or_pattern.match(str(url)) # type:ignore[union-attr]
)

def match(self, method: str, url: URL) -> bool:
if self.method != method.lower():
Expand Down Expand Up @@ -322,6 +325,113 @@ def add(self, url: 'Union[URL, str, Pattern]', method: str = hdrs.METH_GET,
callback=callback,
))

def _format_call_signature(self, *args, **kwargs) -> str:
message = '%s(%%s)' % self.__class__.__name__ or 'mock'
formatted_args = ''
args_string = ', '.join([repr(arg) for arg in args])
kwargs_string = ', '.join([
'%s=%r' % (key, value) for key, value in kwargs.items()
])
if args_string:
formatted_args = args_string
if kwargs_string:
if formatted_args:
formatted_args += ', '
formatted_args += kwargs_string

return message % formatted_args

def assert_not_called(self):
"""assert that the mock was never called.
"""
if len(self.requests) != 0:
msg = ("Expected '%s' to not have been called. Called %s times."
% (self.__class__.__name__,
len(self._responses)))
raise AssertionError(msg)

def assert_called(self):
"""assert that the mock was called at least once.
"""
if len(self.requests) == 0:
msg = ("Expected '%s' to have been called."
% (self.__class__.__name__,))
raise AssertionError(msg)

def assert_called_once(self):
"""assert that the mock was called only once.
"""
call_count = len(self.requests)
if call_count == 1:
call_count = len(list(self.requests.values())[0])
if not call_count == 1:
msg = ("Expected '%s' to have been called once. Called %s times."
% (self.__class__.__name__,
call_count))

raise AssertionError(msg)

def assert_called_with(self, url: 'Union[URL, str, Pattern]',
method: str = hdrs.METH_GET,
*args: Any,
**kwargs: Any):
"""assert that the last call was made with the specified arguments.
Raises an AssertionError if the args and keyword args passed in are
different to the last call to the mock."""
url = normalize_url(merge_params(url, kwargs.get('params')))
method = method.upper()
key = (method, url)
try:
expected = self.requests[key][-1]
except KeyError:
expected_string = self._format_call_signature(
url, method=method, *args, **kwargs
)
raise AssertionError(
'%s call not found' % expected_string
)
actual = self._build_request_call(method, *args, **kwargs)
if not expected == actual:
expected_string = self._format_call_signature(
expected,
)
actual_string = self._format_call_signature(
actual
)
raise AssertionError(
'%s != %s' % (expected_string, actual_string)
)

def assert_any_call(self, url: 'Union[URL, str, Pattern]',
method: str = hdrs.METH_GET,
*args: Any,
**kwargs: Any):
"""assert the mock has been called with the specified arguments.
The assert passes if the mock has *ever* been called, unlike
`assert_called_with` and `assert_called_once_with` that only pass if
the call is the most recent one."""
url = normalize_url(merge_params(url, kwargs.get('params')))
method = method.upper()
key = (method, url)

try:
self.requests[key]
except KeyError:
expected_string = self._format_call_signature(
url, method=method, *args, **kwargs
)
raise AssertionError(
'%s call not found' % expected_string
)

def assert_called_once_with(self, *args: Any, **kwargs: Any):
"""assert that the mock was called once with the specified arguments.
Raises an AssertionError if the args and keyword args passed in are
different to the only call to the mock."""
self.assert_called_once()
self.assert_called_with(*args, **kwargs)

@staticmethod
def is_exception(resp_or_exc: Union[ClientResponse, Exception]) -> bool:
if inspect.isclass(resp_or_exc):
Expand Down Expand Up @@ -355,12 +465,16 @@ async def match(

if self.is_exception(response_or_exc):
raise response_or_exc
is_redirect = response_or_exc.status in (301, 302, 303, 307, 308)
# If response_or_exc was an exception, it would have been raised.
# At this point we can be sure it's a ClientResponse
response: ClientResponse
response = response_or_exc # type:ignore[assignment]
is_redirect = response.status in (301, 302, 303, 307, 308)
if is_redirect and allow_redirects:
if hdrs.LOCATION not in response_or_exc.headers:
if hdrs.LOCATION not in response.headers:
break
history.append(response_or_exc)
redirect_url = URL(response_or_exc.headers[hdrs.LOCATION])
history.append(response)
redirect_url = URL(response.headers[hdrs.LOCATION])
if redirect_url.is_absolute():
url = redirect_url
else:
Expand All @@ -370,9 +484,8 @@ async def match(
else:
break

response_or_exc._history = tuple(history)

return response_or_exc
response._history = tuple(history)
return response

async def _request_mock(self, orig_self: ClientSession,
method: str, url: 'Union[URL, str]',
Expand All @@ -393,12 +506,8 @@ async def _request_mock(self, orig_self: ClientSession,

key = (method, url)
self.requests.setdefault(key, [])
try:
kwargs_copy = copy.deepcopy(kwargs)
except (TypeError, ValueError):
# Handle the fact that some values cannot be deep copied
kwargs_copy = kwargs
self.requests[key].append(RequestCall(args, kwargs_copy))
request_call = self._build_request_call(method, *args, **kwargs)
self.requests[key].append(request_call)

response = await self.match(method, url, **kwargs)

Expand All @@ -422,3 +531,19 @@ async def _request_mock(self, orig_self: ClientSession,
response.raise_for_status()

return response

def _build_request_call(self, method: str = hdrs.METH_GET,
*args: Any,
allow_redirects: bool = True,
**kwargs: Any):
"""Return request call."""
kwargs.setdefault('allow_redirects', allow_redirects)
if method == 'POST':
kwargs.setdefault('data', None)

try:
kwargs_copy = copy.deepcopy(kwargs)
except (TypeError, ValueError):
# Handle the fact that some values cannot be deep copied
kwargs_copy = kwargs
return RequestCall(args, kwargs_copy)
1 change: 1 addition & 0 deletions requirements-dev.txt
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@ pytest-html==2.1.1
ddt==1.4.1
typing
asynctest==0.13.0
yarl==1.7.2
Loading

0 comments on commit b401e6c

Please sign in to comment.