pax_global_header00006660000000000000000000000064145501457060014521gustar00rootroot0000000000000052 comment=b287ab2ffef64d31770a649f1788d84080132bcc aresponses-aresponses-b287ab2/000077500000000000000000000000001455014570600164745ustar00rootroot00000000000000aresponses-aresponses-b287ab2/.github/000077500000000000000000000000001455014570600200345ustar00rootroot00000000000000aresponses-aresponses-b287ab2/.github/flake8_matcher.json000066400000000000000000000005141455014570600236040ustar00rootroot00000000000000{ "problemMatcher": [ { "owner": "lint-error", "severity": "error", "pattern": [ { "regexp": "^\\s*([^:]*):(\\d+):(\\d+): ([A-Z]{1,3}\\d\\d\\d) (.*)$", "file": 1, "line": 2, "column": 3, "code": 4, "message": 5 } ] } ] }aresponses-aresponses-b287ab2/.github/workflows/000077500000000000000000000000001455014570600220715ustar00rootroot00000000000000aresponses-aresponses-b287ab2/.github/workflows/ci-requirements.in000066400000000000000000000003761455014570600255430ustar00rootroot00000000000000coverage pytest pytest-asyncio pytest-randomly pytest-cov # linter deps black flake8 flake8-builtins flake8-comprehensions flake8-eradicate flake8-mutable flake8-printf-formatting flake8-pytest-style flake8-use-fstring flake8-variables-names pep8-namingaresponses-aresponses-b287ab2/.github/workflows/ci-requirements.txt000066400000000000000000000061431455014570600257520ustar00rootroot00000000000000# # This file is autogenerated by pip-compile with Python 3.7 # by the following command: # # pip-compile --output-file=.github/workflows/ci-requirements.txt .github/workflows/ci-requirements.in setup.py # aiohttp==3.8.6 # via aresponses (setup.py) aiosignal==1.3.1 # via aiohttp async-timeout==4.0.3 # via aiohttp asynctest==0.13.0 # via aiohttp attrs==23.2.0 # via # aiohttp # flake8-eradicate black==23.3.0 # via -r .github/workflows/ci-requirements.in charset-normalizer==3.3.2 # via aiohttp click==8.1.7 # via black coverage[toml]==7.2.7 # via # -r .github/workflows/ci-requirements.in # pytest-cov eradicate==2.3.0 # via flake8-eradicate exceptiongroup==1.2.0 # via pytest flake8==5.0.4 # via # -r .github/workflows/ci-requirements.in # flake8-builtins # flake8-comprehensions # flake8-eradicate # flake8-mutable # flake8-printf-formatting # flake8-use-fstring # pep8-naming flake8-builtins==2.1.0 # via -r .github/workflows/ci-requirements.in flake8-comprehensions==3.13.0 # via -r .github/workflows/ci-requirements.in flake8-eradicate==1.4.0 # via -r .github/workflows/ci-requirements.in flake8-mutable==1.2.0 # via -r .github/workflows/ci-requirements.in flake8-plugin-utils==1.3.3 # via flake8-pytest-style flake8-printf-formatting==1.1.2 # via -r .github/workflows/ci-requirements.in flake8-pytest-style==1.7.2 # via -r .github/workflows/ci-requirements.in flake8-use-fstring==1.4 # via -r .github/workflows/ci-requirements.in flake8-variables-names==0.0.6 # via -r .github/workflows/ci-requirements.in frozenlist==1.3.3 # via # aiohttp # aiosignal idna==3.6 # via yarl importlib-metadata==4.2.0 # via # attrs # click # flake8 # flake8-comprehensions # flake8-eradicate # flake8-printf-formatting # pluggy # pytest # pytest-randomly iniconfig==2.0.0 # via pytest mccabe==0.7.0 # via flake8 multidict==6.0.4 # via # aiohttp # yarl mypy-extensions==1.0.0 # via black packaging==23.2 # via # black # pytest pathspec==0.11.2 # via black pep8-naming==0.13.3 # via -r .github/workflows/ci-requirements.in platformdirs==4.0.0 # via black pluggy==1.2.0 # via pytest pycodestyle==2.9.1 # via flake8 pyflakes==2.5.0 # via flake8 pytest==7.4.4 # via # -r .github/workflows/ci-requirements.in # pytest-asyncio # pytest-cov # pytest-randomly pytest-asyncio==0.21.1 # via # -r .github/workflows/ci-requirements.in # aresponses (setup.py) pytest-cov==4.1.0 # via -r .github/workflows/ci-requirements.in pytest-randomly==3.12.0 # via -r .github/workflows/ci-requirements.in tomli==2.0.1 # via # black # coverage # pytest typed-ast==1.5.5 # via black typing-extensions==4.7.1 # via # aiohttp # async-timeout # black # importlib-metadata # platformdirs # pytest-asyncio # yarl yarl==1.9.4 # via aiohttp zipp==3.15.0 # via importlib-metadata aresponses-aresponses-b287ab2/.github/workflows/ci.yaml000066400000000000000000000063241455014570600233550ustar00rootroot00000000000000name: Python Checks on: pull_request: push: branches: - master workflow_dispatch: jobs: lint: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - uses: actions/setup-python@v2 with: python-version: 3.9 - name: Install dependencies run: | python -m pip install --disable-pip-version-check -r .github/workflows/ci-requirements.txt - name: Lint with flake8 run: | echo "::add-matcher::.github/flake8_matcher.json" flake8 --statistics --show-source --append-config=tox.ini . autoformat: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - uses: actions/setup-python@v2 with: python-version: 3.9 - name: Install dependencies run: | python -m pip install --disable-pip-version-check black - name: Autoformatter run: | black --diff . test: runs-on: ${{ matrix.software-versions.os }} strategy: fail-fast: false matrix: software-versions: - {py: "3.7", aiohttp: "==3.1.*", os: "ubuntu-latest"} - {py: "3.7", aiohttp: "==3.2.*", os: "ubuntu-latest"} - {py: "3.7", aiohttp: "==3.3.*", os: "ubuntu-latest"} - {py: "3.7", aiohttp: "==3.4.*", os: "ubuntu-latest"} - {py: "3.7", aiohttp: "==3.5.*", os: "ubuntu-latest"} - {py: "3.7", aiohttp: "==3.6.*", os: "ubuntu-latest"} - {py: "3.7", aiohttp: "==3.7.*", os: "ubuntu-latest"} - {py: "3.7", aiohttp: "==3.8.*", os: "ubuntu-latest"} - {py: "3.8", aiohttp: "==3.6.*", os: "ubuntu-latest"} - {py: "3.8", aiohttp: "==3.7.*", os: "ubuntu-latest"} - {py: "3.8", aiohttp: "==3.8.*", os: "ubuntu-latest"} - {py: "3.8", aiohttp: "==3.9.*", os: "ubuntu-latest"} - { py: "3.9", aiohttp: "==3.6.*" , os: "ubuntu-latest"} - { py: "3.9", aiohttp: "==3.7.*" , os: "ubuntu-latest"} - { py: "3.9", aiohttp: "==3.8.*" , os: "ubuntu-latest"} - { py: "3.9", aiohttp: "==3.9.*" , os: "ubuntu-latest"} - { py: "3.10", aiohttp: "==3.7.*" , os: "ubuntu-latest"} - { py: "3.10", aiohttp: "==3.8.*" , os: "ubuntu-latest"} - { py: "3.10", aiohttp: "==3.9.*" , os: "ubuntu-latest"} - { py: "3.11", aiohttp: "==3.7.*" , os: "ubuntu-latest"} - { py: "3.11", aiohttp: "==3.8.*" , os: "ubuntu-latest"} - { py: "3.11", aiohttp: "==3.9.*" , os: "ubuntu-latest"} - { py: "3.12", aiohttp: "==3.7.*" , os: "ubuntu-latest"} - { py: "3.12", aiohttp: "==3.9.*" , os: "ubuntu-latest"} - { py: "3.12", aiohttp: ">=3.9" , os: "ubuntu-latest"} steps: - uses: actions/checkout@v2 - name: Set up Python ${{ matrix.software-versions.py }} uses: actions/setup-python@v2 with: python-version: ${{ matrix.software-versions.py }} - name: Install dependencies env: AIOHTTP_VERSION: ${{ matrix.software-versions.aiohttp }} PIP_DISABLE_PIP_VERSION_CHECK: 1 run: | python -m pip install -r .github/workflows/ci-requirements.in python -m pip install aiohttp$AIOHTTP_VERSION python -m pip install -e . - name: Test with pytest run: | pytestaresponses-aresponses-b287ab2/.gitignore000066400000000000000000000002361455014570600204650ustar00rootroot00000000000000.DS_Store .idea .direnv .cache *.egg-info __pycache___ dist/ build/ .tox **/__pycache__ **/.pytest_cache .python-version .envrc .coverage pip-wheel-metadata/ aresponses-aresponses-b287ab2/LICENSE000066400000000000000000000020771455014570600175070ustar00rootroot00000000000000The MIT License (MIT) Copyright (c) 2016 CircleUp Network, Inc Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. aresponses-aresponses-b287ab2/Makefile000066400000000000000000000051751455014570600201440ustar00rootroot00000000000000SHELL := /bin/bash python_version = 3.9.18 venv_prefix = aresponses venv_name = $(venv_prefix)-$(python_version) pyenv_instructions=https://github.com/pyenv/pyenv#installation pyenv_virt_instructions=https://github.com/pyenv/pyenv-virtualenv#pyenv-virtualenv init: require_pyenv ## Setup a dev environment for local development. @pyenv install $(python_version) -s @echo -e "\033[0;32m ✔️ 🐍 $(python_version) installed \033[0m" @if ! [ -d "$$(pyenv root)/versions/$(venv_name)" ]; then\ pyenv virtualenv $(python_version) $(venv_name);\ fi; @pyenv local $(venv_name) @echo -e "\033[0;32m ✔️ 🐍 $(venv_name) virtualenv activated \033[0m" pip install --upgrade pip pip-tools -r .github/workflows/ci-requirements.txt @echo -e "\nEnvironment setup! ✨ 🍰 ✨ 🐍 \n\nCopy this path to tell PyCharm where your virtualenv is. You may have to click the refresh button in the pycharm file explorer.\n" @echo -e "\033[0;32m" @pyenv which python @echo -e "\n\033[0m" @echo -e "The following commands are available to run in the Makefile\n" @make -s help af: autoformat ## Alias for `autoformat` autoformat: ## Run the autoformatter. @black . test: ## Run the tests. @pytest @echo -e "The tests pass! ✨ 🍰 ✨" lint: ## Run the code linter. @flake8 --statistics --append-config=tox.ini . @echo -e "No linting errors - well done! ✨ 🍰 ✨" deploy: ## Deploy the package to pypi.org pip install twine wheel -git tag $$(python setup.py -V) git push --tags python setup.py bdist_wheel python setup.py sdist @echo 'pypi.org Username: ' @read username && twine upload dist/* -u $$username; rm -rf build rm -rf dist @echo "Deploy successful! ✨ 🍰 ✨" requirements: ## Freeze the requirements.txt file pip-compile setup.py .github/workflows/ci-requirements.in --output-file=.github/workflows/ci-requirements.txt --upgrade require_pyenv: @if ! [ -x "$$(command -v pyenv)" ]; then\ echo -e '\n\033[0;31m ❌ pyenv is not installed. Follow instructions here: $(pyenv_instructions)\n\033[0m';\ exit 1;\ else\ echo -e "\033[0;32m ✔️ pyenv installed\033[0m";\ fi @if ! [[ "$$(pyenv virtualenv --version)" == *"pyenv-virtualenv"* ]]; then\ echo -e '\n\033[0;31m ❌ pyenv virtualenv is not installed. Follow instructions here: $(pyenv_virt_instructions) \n\033[0m';\ exit 1;\ else\ echo -e "\033[0;32m ✔️ pyenv-virtualenv installed\033[0m";\ fi help: ## Show this help message. @## https://gist.github.com/prwhite/8168133#gistcomment-1716694 @echo -e "$$(grep -hE '^\S+:.*##' $(MAKEFILE_LIST) | sed -e 's/:.*##\s*/:/' -e 's/^\(.\+\):\(.*\)/\\x1b[36m\1\\x1b[m:\2/' | column -c2 -t -s :)" | sort aresponses-aresponses-b287ab2/README.md000066400000000000000000000236701455014570600177630ustar00rootroot00000000000000 # aresponses [![image](https://img.shields.io/pypi/v/aresponses.svg)](https://pypi.org/project/aresponses/) [![image](https://img.shields.io/pypi/pyversions/aresponses.svg)](https://pypi.org/project/aresponses/) [![build status](https://github.com/CircleUp/aresponses/workflows/Python%20Checks/badge.svg)](https://github.com/CircleUp/aresponses/actions?query=branch%3Amaster) [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/ambv/black) an asyncio testing server for mocking external services ## Features - Fast mocks using actual network connections - allows mocking some types of network issues - use regular expression matching for domain, path, method, or body - works with https requests as well (by switching them to http requests) - works with callables ## Usage Add routes and responses via the `aresponses.add` method: ```python def add( host_pattern=ANY, path_pattern=ANY, method_pattern=ANY, response="", *, route=None, body_pattern=ANY, m match_querystring=False, repeat=1 ) ``` When a request is received the first matching response will be returned and removed from the routing table. The `response` argument can be either a string, Response, dict, or list. Use `aresponses.Response` when you need to do something more complex. **Note that version >=2.0 requires explicit assertions!** ```python @pytest.mark.asyncio async def test_simple(aresponses): aresponses.add("google.com", "/api/v1/", "GET", response="OK") aresponses.add('foo.com', '/', 'get', aresponses.Response(text='error', status=500)) async with aiohttp.ClientSession() as session: async with session.get("http://google.com/api/v1/") as response: text = await response.text() assert text == "OK" async with session.get("https://foo.com") as response: text = await response.text() assert text == "error" aresponses.assert_plan_strictly_followed() ``` #### Assertions In aresponses 1.x requests that didn't match a route stopped the event loop and thus forced an exception. In aresponses >2.x it's required to make assertions at the end of the test. There are three assertions functions provided: - `aresponses.assert_no_unused_routes` Raises `UnusedRouteError` if all the routes defined were not used up. - `aresponses.assert_called_in_order` - Raises `UnorderedRouteCallError` if the routes weren't called in the order they were defined. - `aresponses.assert_all_requests_matched` - Raises `NoRouteFoundError` if any requests were made that didn't match to a route. It's likely but not guaranteed that your code will throw an exception in this situation before the assertion is reached. Instead of calling these individually, **it's recommended to call `aresponses.assert_plan_strictly_followed()` at the end of each test as it runs all three of the above assertions.** #### Regex and Repeat `host_pattern`, `path_pattern`, `method_pattern` and `body_pattern` may be either strings (exact match) or regular expressions. The repeat argument permits a route to be used multiple times. If you want to just blanket mock a service, without concern for how many times its called you could set repeat to a large number and not call `aresponses.assert_plan_strictly_followed` or `arespones.assert_no_unused_routes`. ```python @pytest.mark.asyncio async def test_regex_repetition(aresponses): aresponses.add(re.compile(r".*\.?google\.com"), response="OK", repeat=2) async with aiohttp.ClientSession() as session: async with session.get("http://google.com") as response: text = await response.text() assert text == "OK" async with session.get("http://api.google.com") as response: text = await response.text() assert text == "OK" aresponses.assert_plan_strictly_followed() ``` #### Json Responses As a convenience, if a dict or list is passed to `response` then it will create a json response. A `aiohttp.web_response.json_response` object can be used for more complex situations. ```python @pytest.mark.asyncio async def test_json(aresponses): aresponses.add("google.com", "/api/v1/", "GET", response={"status": "OK"}) async with aiohttp.ClientSession() as session: async with session.get("http://google.com/api/v1/") as response: assert {"status": "OK"} == await response.json() aresponses.assert_plan_strictly_followed() ``` #### Custom Handler Custom functions can be used for whatever other complex logic is desired. In example below the handler is set to repeat infinitely and always return 500. ```python import math @pytest.mark.asyncio async def test_handler(aresponses): def break_everything(request): return aresponses.Response(status=500, text=str(request.url)) aresponses.add(response=break_everything, repeat=math.inf) async with aiohttp.ClientSession() as session: async with session.get("http://google.com/api/v1/") as response: assert response.status == 500 ``` #### Passthrough Pass `aresponses.passthrough` into the response argument to allow a request to bypass mocking. ```python aresponses.add('httpstat.us', '/200', 'get', aresponses.passthrough) ``` #### Inspecting history History of calls can be inspected via `aresponses.history` which returns the namedTuple `RoutingLog(request, route, response)` ```python @pytest.mark.asyncio async def test_history(aresponses): aresponses.add(response=aresponses.Response(text="hi"), repeat=2) async with aiohttp.ClientSession() as session: async with session.get("http://foo.com/b") as response: await response.text() async with session.get("http://bar.com/a") as response: await response.text() assert len(aresponses.history) == 2 assert aresponses.history[0].request.host == "foo.com" assert aresponses.history[1].request.host == "bar.com" assert "Route(" in repr(aresponses.history[0].route) aresponses.assert_plan_strictly_followed() ``` #### Context manager usage ```python import aiohttp import pytest import aresponses @pytest.mark.asyncio async def test_foo(event_loop): async with aresponses.ResponsesMockServer(loop=event_loop) as arsps: arsps.add('foo.com', '/', 'get', 'hi there!!') arsps.add(arsps.ANY, '/', 'get', arsps.Response(text='hey!')) async with aiohttp.ClientSession(loop=event_loop) as session: async with session.get('http://foo.com') as response: text = await response.text() assert text == 'hi' async with session.get('https://google.com') as response: text = await response.text() assert text == 'hey!' ``` #### working with [pytest-aiohttp](https://github.com/aio-libs/pytest-aiohttp) If you need to use aresponses together with pytest-aiohttp, you should re-initialize main aresponses fixture with `loop` fixture ```python from aresponses import ResponsesMockServer @pytest.fixture async def aresponses(loop): async with ResponsesMockServer(loop=loop) as server: yield server ``` If you're trying to use the `aiohttp_client` test fixture then you'll need to mock out the aiohttp `loop` fixture instead: ```python @pytest.fixture def loop(event_loop): """replace aiohttp loop fixture with pytest-asyncio fixture""" return event_loop ``` ## Contributing ### Dev environment setup - **install pyenv and pyenv-virtualenv** - Makes it easy to install specific versions of python and switch between them. Make sure you install the virtualenv bash hook - `git clone` the repo and `cd` into it. - `make init` - installs proper version of python, creates the virtual environment, activates it and installs all the requirements ### Submitting a feature request - **`git checkout -b my-feature-branch`** - **make some cool changes** - **`make autoformat`** - **`make test`** - **`make lint`** - **create pull request** ### Updating package on pypi - `make deploy` ## Changelog #### 3.0.0 - fix: start using `asyncio.get_running_loop()` instead of `event_loop` per the error: ``` PytestDeprecationWarning: aresponses is asynchronous and explicitly requests the "event_loop" fixture. Asynchronous fixtures and test functions should use "asyncio.get_running_loop()" instead. ``` - drop support for python 3.6 - add comprehensive matrix testing of all supported python and aiohttp versions - tighten up the setup.py requirements #### 2.1.6 - fix: incorrect pytest plugin entrypoint name (#72) #### 2.1.5 - support asyncio_mode = strict (#68) #### 2.1.4 - fix: don't assume utf8 request contents #### 2.1.3 - accidental no-op release #### 2.1.2 - documentation: add pypi documentation #### 2.1.1 - bugfix: RecursionError when aresponses is used in more than 1000 tests (#63) #### 2.1.0 - feature: add convenience method `add_local_passthrough` - bugfix: fix https subrequest mocks. support aiohttp_client compatibility #### 2.0.2 - bugfix: ensure request body is available in history #### 2.0.0 **Warning! Breaking Changes!** - breaking change: require explicit assertions for test failures - feature: autocomplete works in intellij/pycharm - feature: can match on body of request - feature: store calls made - feature: repeated responses - bugfix: no longer stops event loop - feature: if dict or list is passed into `response`, a json response will be generated #### 1.1.2 - make passthrough feature work with binary data #### 1.1.1 - regex fix for Python 3.7.0 #### 1.1.0 - Added passthrough option to permit live network calls - Added example of using a callable as a response #### 1.0.0 - Added an optional `match_querystring` argument that lets you match on querystring as well ## Contributors * Bryce Drennan, CircleUp * Marco Castelluccio, Mozilla * Jesse Vogt, CircleUp * Pavol Vargovcik, Kiwi.com aresponses-aresponses-b287ab2/aresponses/000077500000000000000000000000001455014570600206565ustar00rootroot00000000000000aresponses-aresponses-b287ab2/aresponses/__init__.py000066400000000000000000000001551455014570600227700ustar00rootroot00000000000000from aiohttp.web import Response # noqa from aresponses.main import aresponses, ResponsesMockServer # noqa aresponses-aresponses-b287ab2/aresponses/errors.py000066400000000000000000000003701455014570600225440ustar00rootroot00000000000000class AresponsesAssertionError(AssertionError): pass class NoRouteFoundError(AresponsesAssertionError): pass class UnusedRouteError(AresponsesAssertionError): pass class UnorderedRouteCallError(AresponsesAssertionError): pass aresponses-aresponses-b287ab2/aresponses/main.py000066400000000000000000000250231455014570600221560ustar00rootroot00000000000000import asyncio import logging import math import re from copy import copy from typing import List, NamedTuple try: from pytest_asyncio import fixture as asyncio_fixture except ImportError: # Backward compatability for pytest-asyncio<0.17 import pytest asyncio_fixture = pytest.fixture from aiohttp import web, ClientSession from aiohttp.client_reqrep import ClientRequest from aiohttp.connector import TCPConnector from aiohttp.helpers import sentinel from aiohttp.test_utils import BaseTestServer from aiohttp.web_request import BaseRequest from aiohttp.web_response import StreamResponse, json_response from aiohttp.web_runner import ServerRunner from aiohttp.web_server import Server from aresponses.errors import ( NoRouteFoundError, UnusedRouteError, UnorderedRouteCallError, ) from aresponses.utils import _text_matches_pattern, ANY logger = logging.getLogger(__name__) class RawResponse(StreamResponse): """ Allow complete control over the response Useful for mocking invalid responses """ def __init__(self, body): super().__init__() self._body = body async def _start(self, request, *_, **__): self._req = request self._keep_alive = False writer = self._payload_writer = request._payload_writer return writer async def write_eof(self, *_, **__): # noqa await super().write_eof(self._body) class Route: def __init__( self, method_pattern=ANY, host_pattern=ANY, path_pattern=ANY, body_pattern=ANY, match_querystring=False, repeat=1, ): self.method_pattern = method_pattern self.host_pattern = host_pattern self.path_pattern = path_pattern self.body_pattern = body_pattern self.match_querystring = match_querystring self.repeat = repeat async def matches(self, request): path_to_match = request.path_qs if self.match_querystring else request.path if not _text_matches_pattern(self.host_pattern, request.host): return False if not _text_matches_pattern(self.path_pattern, path_to_match): return False if not _text_matches_pattern(self.method_pattern, request.method.lower()): return False if self.body_pattern != ANY: if not _text_matches_pattern(self.body_pattern, await request.text()): return False return True def __str__(self): return ( f"method={self.method_pattern} host_pattern={self.host_pattern} " f"path={self.path_pattern} body={self.body_pattern} " f"match_querystring={self.match_querystring}" ) def __repr__(self): return ( f"Route(method={repr(self.method_pattern)}, " f"host_pattern={repr(self.host_pattern)}, " f"path={repr(self.path_pattern)}, " f"body={repr(self.body_pattern)}, " f"match_querystring={repr(self.match_querystring)})" ) class RoutingLog(NamedTuple): request: BaseRequest route: Route response: StreamResponse class ResponsesMockServer(BaseTestServer): ANY = ANY Response = web.Response RawResponse = RawResponse INFINITY = math.inf LOCALHOST = re.compile(r"127\.0\.0\.1:?\d{0,5}") def __init__(self, *, scheme=sentinel, host="127.0.0.1", **kwargs): self._responses = [] self._exception = None self._unmatched_requests = [] self._first_unordered_route = None self._request_count = 0 self._history = [] super().__init__(scheme=scheme, host=host, **kwargs) async def _make_runner(self, debug=True, **kwargs): srv = Server(self._handler, loop=self._loop, debug=True, **kwargs) return ServerRunner(srv, debug=debug, **kwargs) async def _handler(self, request): self._request_count += 1 route, response = await self._find_response(request) # ensures the request content is loaded even if the handler didn't # need it. This makes it available in`aresponses.history` await request.read() self._history.append(RoutingLog(request, route, response)) return response def add( self, host_pattern=ANY, path_pattern=ANY, method_pattern=ANY, response="", *, route=None, body_pattern=ANY, match_querystring=False, repeat=1, ): """ Adds a route and response to the mock server. When the route is hit `repeat` times it will be removed from the routing table.Z :param host_pattern: :param path_pattern: :param method_pattern: :param response: :param route: A Route object. Overrides all args except for `response`. Useful for custom matching. :param body_pattern: :param match_querystring: :param repeat: :return: """ if isinstance(host_pattern, str): host_pattern = host_pattern.lower() if isinstance(method_pattern, str): method_pattern = method_pattern.lower() if route is None: route = Route( method_pattern=method_pattern, host_pattern=host_pattern, path_pattern=path_pattern, body_pattern=body_pattern, match_querystring=match_querystring, repeat=repeat, ) self._responses.append((route, response)) def add_local_passthrough(self, repeat=INFINITY): self.add(host_pattern=self.LOCALHOST, repeat=repeat, response=self.passthrough) async def _find_response(self, request): for i, (route, response) in enumerate(self._responses): if not await route.matches(request): continue route.repeat -= 1 if route.repeat <= 0: del self._responses[i] else: self._responses[i] = (route, copy(response)) if callable(response): if asyncio.iscoroutinefunction(response): response = await response(request) else: response = response(request) elif isinstance(response, str): response = self.Response(body=response) elif isinstance(response, (dict, list)): response = json_response(data=response) if i > 0 and self._first_unordered_route is None: self._first_unordered_route = route return route, response self._unmatched_requests.append(request) return None, None async def passthrough(self, request): """Make non-mocked network request""" class DirectTcpConnector(TCPConnector): def _resolve_host(slf, *args, **kwargs): # noqa return self._old_resolver_mock(slf, *args, **kwargs) class DirectClientRequest(ClientRequest): def is_ssl(slf) -> bool: # noqa return slf._aresponses_direct_is_ssl() connector = DirectTcpConnector() original_request = request.clone( scheme="https" if request.headers["AResponsesIsSSL"] else "http" ) headers = {k: v for k, v in request.headers.items() if k != "AResponsesIsSSL"} async with ClientSession( connector=connector, request_class=DirectClientRequest ) as session: request_method = getattr(session, request.method.lower()) async with request_method( original_request.url, headers=headers, data=(await request.read()) ) as r: headers = { k: v for k, v in r.headers.items() if k.lower() == "content-type" } data = await r.read() response = self.Response(body=data, status=r.status, headers=headers) return response async def __aenter__(self) -> "ResponsesMockServer": await self.start_server(loop=self._loop) self._old_resolver_mock = TCPConnector._resolve_host async def _resolver_mock(_self, host, port, traces=None): return [ { "hostname": host, "host": "127.0.0.1", "port": self.port, "family": _self._family, "proto": 0, "flags": 0, } ] TCPConnector._resolve_host = _resolver_mock self._old_is_ssl = ClientRequest.is_ssl ClientRequest._aresponses_direct_is_ssl = ClientRequest.is_ssl def new_is_ssl(_self): return False ClientRequest.is_ssl = new_is_ssl # store whether a request was an SSL request in the `AResponsesIsSSL` header self._old_init = ClientRequest.__init__ def new_init(_self, *largs, **kwargs): self._old_init(_self, *largs, **kwargs) is_ssl = "1" if self._old_is_ssl(_self) else "" _self.update_headers({**_self.headers, "AResponsesIsSSL": is_ssl}) ClientRequest.__init__ = new_init return self async def __aexit__(self, exc_type, exc_val, exc_tb): TCPConnector._resolve_host = self._old_resolver_mock ClientRequest.is_ssl = self._old_is_ssl ClientRequest.__init__ = self._old_init await self.close() def assert_no_unused_routes(self, ignore_infinite_repeats=False): for route, _ in self._responses: if not ignore_infinite_repeats or route.repeat != self.INFINITY: raise UnusedRouteError(f"Unused Route: {route}") def assert_called_in_order(self): if self._first_unordered_route is not None: raise UnorderedRouteCallError( f"Route used out of order: {self._first_unordered_route}" ) def assert_all_requests_matched(self): if self._unmatched_requests: request = self._unmatched_requests[0] raise NoRouteFoundError( f"No match found for request: " f"{request.method} {request.host} {request.path}" ) def assert_plan_strictly_followed(self): self.assert_no_unused_routes() self.assert_called_in_order() self.assert_all_requests_matched() @property def history(self) -> List[RoutingLog]: return self._history @asyncio_fixture() async def aresponses() -> ResponsesMockServer: loop = asyncio.get_running_loop() async with ResponsesMockServer(loop=loop) as server: yield server aresponses-aresponses-b287ab2/aresponses/utils.py000066400000000000000000000007121455014570600223700ustar00rootroot00000000000000import re ANY = re.compile(".*") def _text_matches_pattern(pattern, text): # This is needed for compatibility with old Python versions try: pattern_class = re.Pattern except AttributeError: pattern_class = re._pattern_type if isinstance(pattern, str): if pattern == text: return True elif isinstance(pattern, pattern_class): if pattern.search(text): return True return False aresponses-aresponses-b287ab2/requirements.in000066400000000000000000000001621455014570600215460ustar00rootroot00000000000000# dev/testing requirements black coverage pydocstyle pylava pylava-pylint pylint pytest pytest-asyncio pytest-cov aresponses-aresponses-b287ab2/setup.py000066400000000000000000000033261455014570600202120ustar00rootroot00000000000000from os import path from setuptools import setup __version__ = "3.0.0" this_directory = path.abspath(path.dirname(__file__)) with open(path.join(this_directory, "README.md"), encoding="utf-8") as f: long_description = f.read() setup( name="aresponses", packages=["aresponses"], version=__version__, description=""" Asyncio response mocking. Similar to the responses library used for 'requests' """.strip(), long_description=long_description, long_description_content_type="text/markdown", author="Bryce Drennan", author_email="aresponses@brycedrennan.com ", url="https://github.com/aresponses/aresponses", download_url="https://github.com/aresponses/aresponses/tarball/" + __version__, keywords=["asyncio", "testing", "responses"], classifiers=[ "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "License :: OSI Approved :: MIT License", ], python_requires=">=3.7", install_requires=[ 'aiohttp<3.9.0,>=3.1.0; python_version>="3.7" and python_version<"3.8"', 'aiohttp>=3.6.0; python_version>="3.8" and python_version<"3.10"', 'aiohttp>=3.7.0; python_version>="3.10" and python_version<"3.12"', 'aiohttp>=3.7.0,!=3.8.*; python_version>="3.12"', 'pytest-asyncio==0.16.0; python_version<"3.7"', 'pytest-asyncio>=0.17.0; python_version>="3.7"', ], # the following makes a plugin available to pytest entry_points={"pytest11": ["aresponses = aresponses.main"]}, ) aresponses-aresponses-b287ab2/tests/000077500000000000000000000000001455014570600176365ustar00rootroot00000000000000aresponses-aresponses-b287ab2/tests/__init__.py000066400000000000000000000000001455014570600217350ustar00rootroot00000000000000aresponses-aresponses-b287ab2/tests/conftest.py000066400000000000000000000001371455014570600220360ustar00rootroot00000000000000from aresponses import aresponses assert aresponses pytest_plugins = "aiohttp.pytest_plugin" aresponses-aresponses-b287ab2/tests/test_examples.py000066400000000000000000000036261455014570600230740ustar00rootroot00000000000000import math import re import aiohttp import pytest @pytest.mark.asyncio async def test_simple(aresponses): aresponses.add("google.com", "/api/v1/", "GET", response="OK") aresponses.add("foo.com", "/", "get", aresponses.Response(text="error", status=500)) async with aiohttp.ClientSession() as session: async with session.get("http://google.com/api/v1/") as response: text = await response.text() assert text == "OK" async with session.get("https://foo.com") as response: text = await response.text() assert text == "error" aresponses.assert_plan_strictly_followed() @pytest.mark.asyncio async def test_regex_repetition(aresponses): aresponses.add(re.compile(r".*\.?google\.com"), response="OK", repeat=2) async with aiohttp.ClientSession() as session: async with session.get("http://google.com") as response: text = await response.text() assert text == "OK" async with session.get("http://api.google.com") as response: text = await response.text() assert text == "OK" aresponses.assert_plan_strictly_followed() @pytest.mark.asyncio async def test_json(aresponses): aresponses.add("google.com", "/api/v1/", "GET", response={"status": "OK"}) async with aiohttp.ClientSession() as session: async with session.get("http://google.com/api/v1/") as response: assert {"status": "OK"} == await response.json() aresponses.assert_plan_strictly_followed() @pytest.mark.asyncio async def test_handler(aresponses): def break_everything(request): return aresponses.Response(status=500, text=str(request.url)) aresponses.add(response=break_everything, repeat=math.inf) async with aiohttp.ClientSession() as session: async with session.get("http://google.com/api/v1/") as response: assert response.status == 500 aresponses-aresponses-b287ab2/tests/test_server.py000066400000000000000000000315711455014570600225640ustar00rootroot00000000000000import asyncio import re import aiohttp import pytest import sys from aiohttp import ServerDisconnectedError import aresponses as aresponses_mod # example test in readme.md from aresponses.errors import ( NoRouteFoundError, UnusedRouteError, UnorderedRouteCallError, ) @pytest.mark.asyncio async def test_foo(aresponses): # text as response (defaults to status 200 response) aresponses.add("foo.com", "/", "get", "hi there!!") # custom status code response aresponses.add("foo.com", "/", "get", aresponses.Response(text="error", status=500)) # JSON response aresponses.add("foo.com", "/", "get", {"status": "ok"}) # passthrough response (makes an actual network call) aresponses.add("httpstat.us", "/200", "get", aresponses.passthrough) # custom handler response def my_handler(request): return aresponses.Response(status=200, text=str(request.url)) aresponses.add("foo.com", "/", "get", my_handler) url = "http://foo.com" async with aiohttp.ClientSession() as session: async with session.get(url) as response: text = await response.text() assert text == "hi there!!" async with session.get(url) as response: text = await response.text() assert text == "error" assert response.status == 500 async with session.get(url) as response: text = await response.text() assert text == '{"status": "ok"}' async with session.get("https://httpstat.us/200") as response: text = await response.text() assert text == "200 OK" async with session.get(url) as response: text = await response.text() assert text == "http://foo.com/" aresponses.assert_plan_strictly_followed() @pytest.mark.asyncio async def test_fixture(aresponses): aresponses.add("foo.com", "/", "get", aresponses.Response(text="hi")) url = "http://foo.com" async with aiohttp.ClientSession() as session: async with session.get(url) as response: text = await response.text() assert text == "hi" aresponses.assert_plan_strictly_followed() @pytest.mark.asyncio async def test_body_match(aresponses): aresponses.add( "foo.com", "/", "get", aresponses.Response(text="hi"), body_pattern=re.compile(r".*?apple.*"), ) url = "http://foo.com" async with aiohttp.ClientSession() as session: try: async with session.get(url, data={"fruit": "pineapple"}) as response: text = await response.text() assert text == "hi" except ServerDisconnectedError: pass aresponses.assert_plan_strictly_followed() @pytest.mark.asyncio async def test_https(aresponses): aresponses.add("foo.com", "/", "get", aresponses.Response(text="hi")) url = "https://foo.com" async with aiohttp.ClientSession() as session: async with session.get(url) as response: text = await response.text() assert text == "hi" aresponses.assert_plan_strictly_followed() @pytest.mark.asyncio async def test_context_manager(): loop = asyncio.get_running_loop() async with aresponses_mod.ResponsesMockServer(loop=loop) as arsps: arsps.add("foo.com", "/", "get", aresponses_mod.Response(text="hi")) url = "http://foo.com" async with aiohttp.ClientSession() as session: async with session.get(url) as response: text = await response.text() assert text == "hi" arsps.assert_plan_strictly_followed() @pytest.mark.asyncio async def test_bad_redirect(aresponses): aresponses.add("foo.com", "/", "get", aresponses.Response(text="hi", status=301)) url = "http://foo.com" async with aiohttp.ClientSession() as session: response = await session.get(url) await response.text() aresponses.assert_plan_strictly_followed() @pytest.mark.asyncio async def test_regex(aresponses): aresponses.add( aresponses.ANY, aresponses.ANY, aresponses.ANY, aresponses.Response(text="hi") ) aresponses.add( aresponses.ANY, aresponses.ANY, aresponses.ANY, aresponses.Response(text="there"), ) async with aiohttp.ClientSession() as session: async with session.get("http://foo.com") as response: text = await response.text() assert text == "hi" async with session.get("http://bar.com") as response: text = await response.text() assert text == "there" aresponses.assert_plan_strictly_followed() @pytest.mark.asyncio async def test_callable(aresponses): def handler(request): return aresponses.Response(body=request.host) aresponses.add(aresponses.ANY, aresponses.ANY, aresponses.ANY, handler) aresponses.add(aresponses.ANY, aresponses.ANY, aresponses.ANY, handler) async with aiohttp.ClientSession() as session: async with session.get("http://foo.com") as response: text = await response.text() assert text == "foo.com" async with session.get("http://bar.com") as response: text = await response.text() assert text == "bar.com" aresponses.assert_plan_strictly_followed() @pytest.mark.asyncio async def test_raw_response(aresponses): raw_response = b"""HTTP/1.1 200 OK\r Date: Tue, 26 Dec 2017 05:47:50 GMT\r \r

It works!

""" aresponses.add( aresponses.ANY, aresponses.ANY, aresponses.ANY, aresponses.RawResponse(raw_response), ) async with aiohttp.ClientSession() as session: async with session.get("http://foo.com") as response: text = await response.text() assert "It works!" in text aresponses.assert_plan_strictly_followed() @pytest.mark.asyncio async def test_querystring(aresponses): aresponses.add("foo.com", "/path", "get", aresponses.Response(text="hi")) url = "http://foo.com/path?reply=42" async with aiohttp.ClientSession() as session: async with session.get(url) as response: text = await response.text() assert text == "hi" aresponses.add( "foo.com", "/path2?reply=42", "get", aresponses.Response(text="hi"), match_querystring=True, ) url = "http://foo.com/path2?reply=42" async with aiohttp.ClientSession() as session: async with session.get(url) as response: text = await response.text() assert text == "hi" aresponses.assert_plan_strictly_followed() @pytest.mark.asyncio async def test_querystring_not_match(aresponses): aresponses.add( "foo.com", "/path", "get", aresponses.Response(text="hi"), match_querystring=True, ) aresponses.add( "foo.com", aresponses.ANY, "get", aresponses.Response(text="miss"), match_querystring=True, ) aresponses.add( "foo.com", aresponses.ANY, "get", aresponses.Response(text="miss"), match_querystring=True, ) url = "http://foo.com/path" async with aiohttp.ClientSession() as session: async with session.get(url) as response: text = await response.text() assert text == "hi" url = "http://foo.com/path?reply=42" async with aiohttp.ClientSession() as session: async with session.get(url) as response: text = await response.text() assert text == "miss" url = "http://foo.com/path?reply=43" async with aiohttp.ClientSession() as session: async with session.get(url) as response: text = await response.text() assert text == "miss" aresponses.assert_plan_strictly_followed() @pytest.mark.asyncio async def test_passthrough(aresponses): aresponses.add("httpstat.us", "/200", "get", aresponses.passthrough) url = "https://httpstat.us/200" async with aiohttp.ClientSession() as session: async with session.get(url) as response: text = await response.text() assert text == "200 OK" aresponses.assert_plan_strictly_followed() @pytest.mark.asyncio async def test_failure_not_called(aresponses): aresponses.add("foo.com", "/", "get", aresponses.Response(text="hi")) with pytest.raises(UnusedRouteError): aresponses.assert_no_unused_routes() with pytest.raises(UnusedRouteError): aresponses.assert_plan_strictly_followed() @pytest.mark.asyncio async def test_failure_no_match(aresponses): async with aiohttp.ClientSession() as session: try: async with session.get("http://foo.com") as response: await response.text() except ServerDisconnectedError: pass with pytest.raises(NoRouteFoundError): aresponses.assert_all_requests_matched() with pytest.raises(NoRouteFoundError): aresponses.assert_plan_strictly_followed() @pytest.mark.asyncio async def test_failure_bad_ordering(aresponses): aresponses.add("foo.com", "/a", "get", aresponses.Response(text="hi")) aresponses.add("foo.com", "/b", "get", aresponses.Response(text="hi")) async with aiohttp.ClientSession() as session: async with session.get("http://foo.com/b") as response: await response.text() async with session.get("http://foo.com/a") as response: await response.text() aresponses.assert_all_requests_matched() aresponses.assert_no_unused_routes() with pytest.raises(UnorderedRouteCallError): aresponses.assert_called_in_order() with pytest.raises(UnorderedRouteCallError): aresponses.assert_plan_strictly_followed() @pytest.mark.asyncio async def test_failure_not_called_enough_times(aresponses): aresponses.add("foo.com", "/", "get", aresponses.Response(text="hi"), repeat=2) async with aiohttp.ClientSession() as session: async with session.get("http://foo.com/") as response: await response.text() with pytest.raises(UnusedRouteError): aresponses.assert_plan_strictly_followed() @pytest.mark.asyncio async def test_history(aresponses): aresponses.add(response=aresponses.Response(text="hi"), repeat=2) async with aiohttp.ClientSession() as session: async with session.get("http://foo.com/b") as response: await response.text() async with session.get("http://bar.com/a") as response: await response.text() assert len(aresponses.history) == 2 assert aresponses.history[0].request.host == "foo.com" assert aresponses.history[1].request.host == "bar.com" assert "Route(" in repr(aresponses.history[0].route) aresponses.assert_plan_strictly_followed() @pytest.mark.asyncio async def test_history_post(aresponses): """Ensure the request contents exist in the history""" aresponses.add(method_pattern="POST", response={"some": "response"}) async with aiohttp.ClientSession() as session: async with session.post( "http://bar.com/zzz", json={"greeting": "hello"} ) as response: response_data = await response.json() assert response_data == {"some": "response"} assert len(aresponses.history) == 1 assert aresponses.history[0].request.host == "bar.com" request_data = await aresponses.history[0].request.json() assert request_data == {"greeting": "hello"} assert "Route(" in repr(aresponses.history[0].route) aresponses.assert_plan_strictly_followed() @pytest.mark.asyncio async def test_history_post_binary(aresponses): """ Ensure the request contents exist in the history ...and that it can handle binary requests """ binary_not_utf = b"o\xad<|6\xd2a\x116\x17\xdb\x98-60:" aresponses.add(method_pattern="POST", response={"some": "response"}) async with aiohttp.ClientSession() as session: async with session.post("http://bar.com/zzz", data=binary_not_utf) as response: response_data = await response.json() assert response_data == {"some": "response"} assert len(aresponses.history) == 1 assert aresponses.history[0].request.host == "bar.com" request_data = await aresponses.history[0].request.read() assert request_data == binary_not_utf @pytest.fixture() def _short_recursion_limit(): # The default is 1000, but it takes time (seconds); 100 is much faster. old_limit = sys.getrecursionlimit() sys.setrecursionlimit(100) yield sys.setrecursionlimit(old_limit) @pytest.mark.asyncio @pytest.mark.usefixtures("_short_recursion_limit") async def test_not_exceeding_recursion_limit(): loop = asyncio.get_running_loop() for _ in range(sys.getrecursionlimit()): async with aresponses_mod.ResponsesMockServer(loop=loop) as arsps: arsps.add("fake-host", "/", "get", "hello") async with aiohttp.ClientSession() as session: async with session.get("http://fake-host"): pass aresponses-aresponses-b287ab2/tests/test_with_aiohttp_client.py000066400000000000000000000042371455014570600253160ustar00rootroot00000000000000import aiohttp import pytest from aiohttp import web @pytest.fixture() def loop(event_loop): """replace aiohttp loop fixture with pytest-asyncio fixture""" return event_loop def make_app(): app = web.Application() async def constant_handler(request): return web.Response(text="42") async def ip_handler(request): protocol = request.query["protocol"] ip = await get_ip_address(protocol=protocol) return web.Response(text=f"ip is {ip}") app.add_routes([web.get("/constant", constant_handler)]) app.add_routes([web.get("/ip", ip_handler)]) return app async def get_ip_address(protocol): async with aiohttp.ClientSession() as s: async with s.get(f"{protocol}://httpbin.org/ip") as resp: ip = (await resp.json())["origin"] return ip @pytest.mark.asyncio async def test_app_simple_endpoint(aiohttp_client): client = await aiohttp_client(make_app()) r = await client.get("/constant") assert (await r.text()) == "42" @pytest.mark.asyncio async def test_app_simple_endpoint_with_aresponses(aiohttp_client, aresponses): """ when testing your own aiohttp server you must setup passthrough to it Ideally this wouldn't be necessary but haven't figured that out yet. Perhaps all local calls should be passthrough. """ aresponses.add("127.0.0.1:4241", response=aresponses.passthrough) client = await aiohttp_client(make_app(), server_kwargs={"port": 4241}) r = await client.get("/constant") assert (await r.text()) == "42" @pytest.mark.asyncio @pytest.mark.parametrize("protocol", ["http", "https"]) async def test_app_with_subrequest_using_aresponses( aiohttp_client, aresponses, protocol ): """ but passthrough doesn't work if the handler itself makes an aiohttp https request """ aresponses.add_local_passthrough(repeat=1) aresponses.add("httpbin.org", response={"origin": "1.2.3.4"}) client = await aiohttp_client(make_app()) r = await client.get(f"/ip?protocol={protocol}") body = await r.text() assert r.status == 200, body assert "ip is" in (await r.text()) aresponses.assert_plan_strictly_followed() aresponses-aresponses-b287ab2/tox.ini000066400000000000000000000013571455014570600200150ustar00rootroot00000000000000[pylava] ignore = C0102,C0111,C0203,C0301,C0325,C0330,C0412,C0413,C901,E0101,E0202,E0213,E0611,E1003,E1102,E1120,E1123,E1129,E1133,E1135,E1136,E1137,E126,E131,E241,E402,E501,F401,R0201,R0911,R0912,R0914,R0916,R1702,W0105,W0107,W0108,W0125,W0201,W0212,W0223,W0511,W0603,W0622,W0640,W0703,W1203,W1306,W1307,W1401,W1402,W503 skip = *.venv/*,*.tox/*,.git*,*.direnv/*,*build/* max-line-length = 999 linters = pycodestyle,pyflakes,pylint [pylava:*/__init__.py] ignore = W0611 [pylava:pylint] disable = W0612,W0621 [coverage:run] source=aresponses [pytest] addopts = --doctest-modules -s --tb=native norecursedirs = build dist asyncio_mode = strict [flake8] max-line-length = 88 extend-ignore = E203,E800,VNE001,VNE002 pytest-mark-no-parentheses = True