diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f9066e6..1e5d309 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -14,7 +14,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ['3.9', '3.10', '3.11', '3.12', '3.13'] + python-version: ['3.10', '3.11', '3.12', '3.13', '3.14'] steps: - uses: actions/checkout@v2 - name: Set up Python ${{ matrix.python-version }} diff --git a/pyproject.toml b/pyproject.toml index 20a727d..0bd80a7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,16 +22,16 @@ classifiers = [ "Topic :: Software Development :: Libraries", "Topic :: Software Development :: Libraries :: Application Frameworks", "Programming Language :: Python", - "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", ] [tool.poetry.dependencies] -python = ">=3.9,<4.0" +python = ">=3.10,<4.0" aio-pika = { version = ">=8.0", optional = true } aiohttp = { version = ">=3.7", optional = true } flask = { version = ">=2.0.0", optional = true } @@ -69,10 +69,10 @@ standard-imghdr = "^3.13.0" codecov = "^2.1.13" mypy = "^1.15.0" pre-commit = "~3.2.0" -aioresponses = "^0.7.4" +aiointercept = "^0.1.9" asynctest = "^0.13.0" deepdiff = "^8.0.1" -pytest = "^7.4.0" +pytest = "^9.1.1" pytest-aiohttp = "^1.0.4" pytest-cov = "^4.1.0" pytest-mock = "^3.11.1" @@ -80,6 +80,7 @@ responses = "^0.23.3" respx = "^0.22.0" types-requests = "^2.32.0.20241016" jsonschema = "^4.25.1" +pytest-lazy-fixtures = "^1.4.0" [build-system] requires = ["poetry-core>=1.0.0"] diff --git a/tests/client/conftest.py b/tests/client/conftest.py index ef7d610..d6487ba 100644 --- a/tests/client/conftest.py +++ b/tests/client/conftest.py @@ -1,8 +1,8 @@ import pytest -from aioresponses import aioresponses +from aiointercept import aiointercept @pytest.fixture -def responses(): - with aioresponses() as mocker: +async def aiointerceptor(): + async with aiointercept(mock_external_urls=True) as mocker: yield mocker diff --git a/tests/client/test_async_clients.py b/tests/client/test_async_clients.py index 754f3dd..d54ba26 100644 --- a/tests/client/test_async_clients.py +++ b/tests/client/test_async_clients.py @@ -3,7 +3,8 @@ import pytest import respx -from aioresponses import aioresponses +from aiointercept import aiointercept +from pytest_lazy_fixtures import lf import pjrpc from pjrpc.client.backend import aiohttp as aiohttp_backend @@ -17,14 +18,14 @@ class Request(NamedTuple): content: str def __init__(self): - self.mocker = aioresponses() + self.mocker = aiointercept(mock_external_urls=True) - def __enter__(self): - self.mocker.__enter__() + async def __aenter__(self): + await self.mocker.__aenter__() return self - def __exit__(self, exc_type, exc_val, exc_tb): - self.mocker.__exit__(exc_type, exc_val, exc_tb) + async def __aexit__(self, exc_type, exc_val, exc_tb): + await self.mocker.__aexit__(exc_type, exc_val, exc_tb) def mock(self, method=None, url=None, status=None, content=None, json=None): self.mocker.add(method=method, url=url, status=status, body=content, payload=json, repeat=True) @@ -38,6 +39,12 @@ def requests(self): ] +@pytest.fixture() +async def aiohttp_mocker(): + async with AioHttpMocker() as moker: + yield moker + + class RespxMocker(respx.MockRouter): def mock(self, method=None, url=None, status=None, content=None, json=None): @@ -49,32 +56,36 @@ def requests(self): return [request for request, response in self.calls] +@pytest.fixture() +async def respx_mocker(): + async with RespxMocker() as moker: + yield moker + + @pytest.mark.parametrize( 'Client, mocker', [ - (aiohttp_backend.Client, AioHttpMocker), - (httpx_backend.AsyncClient, RespxMocker), + (aiohttp_backend.Client, lf('aiohttp_mocker')), + (httpx_backend.AsyncClient, lf('respx_mocker')), ], ) async def test_call(Client, mocker): test_url = 'http://test.com/api' - with mocker() as mock: - mock.mock( - 'POST', test_url, status=200, json={ - 'jsonrpc': '2.0', - 'id': 1, - 'result': 'result', - }, - ) - - client = Client(test_url) + mocker.mock( + 'POST', test_url, status=200, json={ + 'jsonrpc': '2.0', + 'id': 1, + 'result': 'result', + }, + ) + async with Client(test_url) as client: response = await client.send(pjrpc.Request(method='method', params=(1, 2), id=1)) assert response.id == 1 assert response.result == 'result' - assert mock.requests[0].url == test_url - assert json.loads(mock.requests[0].content) == { + assert mocker.requests[0].url == test_url + assert json.loads(mocker.requests[0].content) == { 'jsonrpc': '2.0', 'id': 1, 'method': 'method', @@ -84,8 +95,8 @@ async def test_call(Client, mocker): result = await client.call('method', arg1=1, arg2=2) assert result == 'result' - assert mock.requests[1].url == test_url - assert json.loads(mock.requests[1].content) == { + assert mocker.requests[1].url == test_url + assert json.loads(mocker.requests[1].content) == { 'jsonrpc': '2.0', 'id': 1, 'method': 'method', @@ -95,8 +106,8 @@ async def test_call(Client, mocker): result = await client('method', 1, 2) assert result == 'result' - assert mock.requests[2].url == test_url - assert json.loads(mock.requests[2].content) == { + assert mocker.requests[2].url == test_url + assert json.loads(mocker.requests[2].content) == { 'jsonrpc': '2.0', 'id': 1, 'method': 'method', @@ -106,8 +117,8 @@ async def test_call(Client, mocker): result = await client.proxy.method(1, 2) assert result == 'result' - assert mock.requests[3].url == test_url - assert json.loads(mock.requests[3].content) == { + assert mocker.requests[3].url == test_url + assert json.loads(mocker.requests[3].content) == { 'jsonrpc': '2.0', 'id': 1, 'method': 'method', @@ -117,22 +128,20 @@ async def test_call(Client, mocker): @pytest.mark.parametrize( 'Client, mocker', [ - (aiohttp_backend.Client, AioHttpMocker), - (httpx_backend.AsyncClient, RespxMocker), + (aiohttp_backend.Client, lf('aiohttp_mocker')), + (httpx_backend.AsyncClient, lf('respx_mocker')), ], ) async def test_notify(Client, mocker): test_url = 'http://test.com/api' - with mocker() as mock: - mock.mock('POST', test_url, status=200, content=' ') - - client = Client(test_url) + mocker.mock('POST', test_url, status=200, content=' ') + async with Client(test_url) as client: response = await client.send(pjrpc.Request(method='method', params=[1, 2])) assert response is None - assert mock.requests[0].url == test_url - assert json.loads(mock.requests[0].content) == { + assert mocker.requests[0].url == test_url + assert json.loads(mocker.requests[0].content) == { 'jsonrpc': '2.0', 'method': 'method', 'params': [1, 2], @@ -140,8 +149,8 @@ async def test_notify(Client, mocker): response = await client.notify('method', a=1, b=2) assert response is None - assert mock.requests[1].url == test_url - assert json.loads(mock.requests[1].content) == { + assert mocker.requests[1].url == test_url + assert json.loads(mocker.requests[1].content) == { 'jsonrpc': '2.0', 'method': 'method', 'params': {'a': 1, 'b': 2}, @@ -150,31 +159,29 @@ async def test_notify(Client, mocker): @pytest.mark.parametrize( 'Client, mocker', [ - (aiohttp_backend.Client, AioHttpMocker), - (httpx_backend.AsyncClient, RespxMocker), + (aiohttp_backend.Client, lf('aiohttp_mocker')), + (httpx_backend.AsyncClient, lf('respx_mocker')), ], ) async def test_batch(Client, mocker): test_url = 'http://test.com/api' - with (mocker() as mock): - mock.mock( - 'POST', test_url, status=200, json=[ - { - 'jsonrpc': '2.0', - 'id': 1, - 'result': 'result1', - }, - { - 'jsonrpc': '2.0', - 'id': 2, - 'result': 2, - }, - ], - ) - - client = Client(test_url) + mocker.mock( + 'POST', test_url, status=200, json=[ + { + 'jsonrpc': '2.0', + 'id': 1, + 'result': 'result1', + }, + { + 'jsonrpc': '2.0', + 'id': 2, + 'result': 2, + }, + ], + ) + async with Client(test_url) as client: async with client.batch() as batch: batch.send(pjrpc.Request(method='method1', params=[1, 2], id=1)) batch.send(pjrpc.Request(method='method2', params=[2, 3], id=2)) @@ -188,8 +195,8 @@ async def test_batch(Client, mocker): assert result[1].id == 2 assert result[1].result == 2 - assert mock.requests[0].url == test_url - assert json.loads(mock.requests[0].content) == [ + assert mocker.requests[0].url == test_url + assert json.loads(mocker.requests[0].content) == [ { 'jsonrpc': '2.0', 'id': 1, @@ -216,8 +223,8 @@ async def test_batch(Client, mocker): result = batch.get_results() assert result == ['result1', 2] - assert mock.requests[1].url == test_url - assert json.loads(mock.requests[1].content) == [ + assert mocker.requests[1].url == test_url + assert json.loads(mocker.requests[1].content) == [ { 'jsonrpc': '2.0', 'id': 1, @@ -239,8 +246,8 @@ async def test_batch(Client, mocker): result = batch.get_results() assert result == ['result1', 2] - assert mock.requests[2].url == test_url - assert json.loads(mock.requests[2].content) == [ + assert mocker.requests[2].url == test_url + assert json.loads(mocker.requests[2].content) == [ { 'jsonrpc': '2.0', 'id': 1, @@ -262,8 +269,8 @@ async def test_batch(Client, mocker): result = batch.get_results() assert result == ['result1', 2] - assert mock.requests[3].url == test_url - assert json.loads(mock.requests[3].content) == [ + assert mocker.requests[3].url == test_url + assert json.loads(mocker.requests[3].content) == [ { 'jsonrpc': '2.0', 'id': 1, @@ -278,8 +285,9 @@ async def test_batch(Client, mocker): }, ] - with mocker() as mock: - mock.mock('POST', test_url, status=200, content=' ') + mocker.mock('POST', test_url, status=200, content=' ') + + async with Client(test_url) as client: async with client.batch() as batch: batch.notify('method1', 1, 2) batch.notify('method2', 2, 3) @@ -287,8 +295,8 @@ async def test_batch(Client, mocker): result = batch.get_response() assert result is None - assert mock.requests[0].url == test_url - assert json.loads(mock.requests[0].content) == [ + assert mocker.requests[4].url == test_url + assert json.loads(mocker.requests[4].content) == [ { 'jsonrpc': '2.0', 'method': 'method1', @@ -304,24 +312,23 @@ async def test_batch(Client, mocker): @pytest.mark.parametrize( 'Client, mocker', [ - (aiohttp_backend.Client, AioHttpMocker), - (httpx_backend.AsyncClient, RespxMocker), + (aiohttp_backend.Client, lf('aiohttp_mocker')), + (httpx_backend.AsyncClient, lf('respx_mocker')), ], ) async def test_context_manager(Client, mocker): test_url = 'http://test.com/api' - with mocker() as mock: - mock.mock( - 'POST', test_url, status=200, json={ - 'jsonrpc': '2.0', - 'id': 1, - 'result': 'result', - }, - ) + mocker.mock( + 'POST', test_url, status=200, json={ + 'jsonrpc': '2.0', + 'id': 1, + 'result': 'result', + }, + ) - async with Client(test_url) as client: - response = await client.send(pjrpc.Request(method='method', params=(1, 2), id=1)) + async with Client(test_url) as client: + response = await client.send(pjrpc.Request(method='method', params=(1, 2), id=1)) - assert response.id == 1 - assert response.result == 'result' + assert response.id == 1 + assert response.result == 'result' diff --git a/tests/client/test_retry.py b/tests/client/test_retry.py index d16ec91..b9a76c3 100644 --- a/tests/client/test_retry.py +++ b/tests/client/test_retry.py @@ -1,6 +1,6 @@ import pytest import responses -from aioresponses import aioresponses +from aiohttp import ClientConnectionError from pjrpc.client import exceptions, retry from pjrpc.client.backend import aiohttp as aiohttp_backend @@ -40,36 +40,38 @@ def test_retry_strategies(strategy, expected): (2000, 0, {}, 0, True), ], ) -async def test_async_client_retry_strategy_by_code(resp_code, resp_errors, retry_codes, retry_attempts, success): - with aioresponses() as mock: - test_url = 'http://test.com/api' - expected_result = 'result' +async def test_async_client_retry_strategy_by_code( + aiointerceptor, resp_code, resp_errors, retry_codes, retry_attempts, success, +): + test_url = 'http://test.com/api' + expected_result = 'result' - resp_success = dict( - url=test_url, - payload={"jsonrpc": "2.0", "result": expected_result, "id": 1}, - ) - resp_error = dict( - url=test_url, - payload={"jsonrpc": "2.0", "error": {"code": resp_code, "message": "error"}, "id": 1}, - ) + resp_success = dict( + url=test_url, + payload={"jsonrpc": "2.0", "result": expected_result, "id": 1}, + ) + resp_error = dict( + url=test_url, + payload={"jsonrpc": "2.0", "error": {"code": resp_code, "message": "error"}, "id": 1}, + ) - client = aiohttp_backend.Client( - url=test_url, - middlewares=[ - retry.AsyncRetryMiddleware( - retry.RetryStrategy( - codes=retry_codes, - backoff=retry.PeriodicBackoff(attempts=retry_attempts, interval=0.0), - ), + client = aiohttp_backend.Client( + url=test_url, + middlewares=[ + retry.AsyncRetryMiddleware( + retry.RetryStrategy( + codes=retry_codes, + backoff=retry.PeriodicBackoff(attempts=retry_attempts, interval=0.0), ), - ], - ) + ), + ], + ) - for _ in range(resp_errors): - mock.post(**resp_error) - mock.post(**resp_success) + for _ in range(resp_errors): + aiointerceptor.post(**resp_error) + aiointerceptor.post(**resp_success) + async with client: if success: actual_result = await client.proxy.method() assert actual_result == expected_result @@ -83,45 +85,47 @@ async def test_async_client_retry_strategy_by_code(resp_code, resp_errors, retry @pytest.mark.parametrize( 'resp_exc, resp_errors, retry_exc, retry_attempts, success', [ - (ConnectionError, 2, {TimeoutError, ConnectionError}, 2, True), - (TimeoutError, 2, {TimeoutError}, 2, True), - (TimeoutError, 2, {ConnectionError}, 2, False), - (TimeoutError, 1, {TimeoutError}, 2, True), - (TimeoutError, 3, {TimeoutError}, 2, False), - (TimeoutError, 1, {}, 2, False), - (TimeoutError, 0, {}, 0, True), + (ClientConnectionError, 2, {ClientConnectionError}, 2, True), + (ClientConnectionError, 2, {TimeoutError}, 2, False), + (ClientConnectionError, 1, {ClientConnectionError}, 2, True), + (ClientConnectionError, 3, {ClientConnectionError}, 2, False), + (ClientConnectionError, 1, {}, 2, False), + (ClientConnectionError, 0, {}, 0, True), ], ) -async def test_async_client_retry_strategy_by_exception(resp_exc, resp_errors, retry_exc, retry_attempts, success): - with aioresponses() as mock: - test_url = 'http://test.com/api' - expected_result = 'result' +async def test_async_client_retry_strategy_by_exception( + aiointerceptor, resp_exc, resp_errors, retry_exc, retry_attempts, success, +): + test_url = 'http://test.com/api' + expected_result = 'result' - resp_success = dict( - url=test_url, - payload={"jsonrpc": "2.0", "result": expected_result, "id": 1}, - ) - resp_error = dict( - url=test_url, - exception=resp_exc(), - ) + resp_success = dict( + url=test_url, + payload={"jsonrpc": "2.0", "result": expected_result, "id": 1}, + ) - client = aiohttp_backend.Client( - url=test_url, - middlewares=[ - retry.AsyncRetryMiddleware( - retry.RetryStrategy( - exceptions=retry_exc, - backoff=retry.PeriodicBackoff(attempts=retry_attempts, interval=0.0), - ), + resp_error = dict( + url=test_url, + exception=resp_exc(), + ) + + client = aiohttp_backend.Client( + url=test_url, + middlewares=[ + retry.AsyncRetryMiddleware( + retry.RetryStrategy( + exceptions=retry_exc, + backoff=retry.PeriodicBackoff(attempts=retry_attempts, interval=0.0), ), - ], - ) + ), + ], + ) - for _ in range(resp_errors): - mock.post(**resp_error) - mock.post(**resp_success) + for _ in range(resp_errors): + aiointerceptor.post(**resp_error) + aiointerceptor.post(**resp_success) + async with client: if success: actual_result = await client.proxy.method() assert actual_result == expected_result @@ -176,14 +180,15 @@ def test_client_retry_strategy_by_code(resp_code, resp_errors, retry_codes, retr responses.add(resp_error) responses.add(resp_success) - if success: - actual_result = client.proxy.method() - assert actual_result == expected_result - else: - with pytest.raises(exceptions.JsonRpcError) as err: - client.proxy.method() + with client: + if success: + actual_result = client.proxy.method() + assert actual_result == expected_result + else: + with pytest.raises(exceptions.JsonRpcError) as err: + client.proxy.method() - assert err.value.code == resp_code + assert err.value.code == resp_code @pytest.mark.parametrize( @@ -199,7 +204,7 @@ def test_client_retry_strategy_by_code(resp_code, resp_errors, retry_codes, retr ], ) @responses.activate -def test_client_retry_strategy_by_exception(resp_exc, resp_errors, retry_exc, retry_attempts, success): +async def test_client_retry_strategy_by_exception(resp_exc, resp_errors, retry_exc, retry_attempts, success): test_url = 'http://test.com/api' expected_result = 'result' @@ -232,12 +237,13 @@ def test_client_retry_strategy_by_exception(resp_exc, resp_errors, retry_exc, re responses.add(resp_error) responses.add(resp_success) - if success: - actual_result = client.proxy.method() - assert actual_result == expected_result - else: - with pytest.raises(resp_exc): - client.proxy.method() + with client: + if success: + actual_result = client.proxy.method() + assert actual_result == expected_result + else: + with pytest.raises(resp_exc): + client.proxy.method() @responses.activate @@ -283,5 +289,6 @@ def test_client_retry_strategy_by_code_and_exception(): ), ) - actual_result = client.proxy.method() - assert actual_result == expected_result + with client: + actual_result = client.proxy.method() + assert actual_result == expected_result