Skip to content

Commit

Permalink
Fix httpx incorrectly named method on interceptor subclass (#126)
Browse files Browse the repository at this point in the history
* Use pytest-httpbin to avoid outbound requests in most tests

* Enable aiohttp tests in py3.12

* Add standard tests for all engines

Reproduce #125 in tests

* Fix #125

* Bump to 1.4.3
  • Loading branch information
sarayourfriend authored Feb 23, 2024
1 parent e66193b commit ef5bb1a
Show file tree
Hide file tree
Showing 13 changed files with 195 additions and 74 deletions.
5 changes: 5 additions & 0 deletions History.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
History
=======

v1.4.3 / 2024-02-23
-------------------

* Fix httpx incorrectly named method on interceptor subclass by @sarayourfriend in https://github.com/h2non/pook/pull/126

v1.4.2 / 2024-02-15
-------------------

Expand Down
5 changes: 3 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ extra-dependencies = [
"pytest~=7.4",
"pytest-asyncio~=0.20.3",
"pytest-pook==0.1.0b0",
"pytest-httpbin==2.0.0",

"requests~=2.20",
"urllib3~=1.24",
Expand All @@ -62,8 +63,8 @@ extra-dependencies = [
# aiohttp depends on multidict, so we can't test aiohttp until
# https://github.com/aio-libs/multidict/issues/887 is resolved
# async-timeout is only used for testing aiohttp
"aiohttp~=3.8; python_version < '3.12'",
"async-timeout~=4.0.3; python_version < '3.12'",
"aiohttp~=3.8",
"async-timeout~=4.0.3",

# mocket relies on httptools which does not support PyPy
"mocket[pook]~=3.12.2; platform_python_implementation != 'PyPy'",
Expand Down
2 changes: 1 addition & 1 deletion src/pook/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,4 @@
__license__ = "MIT"

# Current version
__version__ = "1.4.2"
__version__ = "1.4.3"
4 changes: 2 additions & 2 deletions src/pook/interceptors/_httpx.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ def handler(client, *_):
def activate(self):
[self._patch(path) for path in PATCHES]

def deactivate(self):
def disable(self):
[patch.stop() for patch in self.patchers]


Expand Down Expand Up @@ -96,7 +96,7 @@ async def handle_async_request(self, request):
mock = self._interceptor.engine.match(pook_request)

if not mock:
transport = self._original_transport_for_url(self._client, self.request.url)
transport = self._original_transport_for_url(self._client, request.url)
return await transport.handle_async_request(request)

if mock._delay:
Expand Down
8 changes: 4 additions & 4 deletions src/pook/interceptors/base.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from abc import abstractmethod, ABCMeta


class BaseInterceptor(object):
class BaseInterceptor:
"""
BaseInterceptor provides a base class for HTTP traffic
interceptors implementations.
Expand All @@ -14,7 +14,7 @@ def __init__(self, engine):
self.engine = engine

@property
def name(self):
def name(self) -> str:
"""
Exposes the interceptor class name.
"""
Expand All @@ -26,12 +26,12 @@ def activate(self):
Activates the traffic interceptor.
This method must be implemented by any interceptor.
"""
pass
raise NotImplementedError("Sub-classes must implement `activate`")

@abstractmethod
def disable(self):
"""
Disables the traffic interceptor.
This method must be implemented by any interceptor.
"""
pass
raise NotImplementedError("Sub-classes must implement `disable`")
49 changes: 28 additions & 21 deletions tests/unit/interceptors/aiohttp_test.py
Original file line number Diff line number Diff line change
@@ -1,53 +1,60 @@
import sys
from pathlib import Path

import pook
import pytest
import aiohttp

from pathlib import Path
import pook

SUPPORTED = sys.version_info < (3, 12)
if SUPPORTED:
# See pyproject.toml comment
import aiohttp
from tests.unit.interceptors.base import StandardTests


pytestmark = [
pytest.mark.pook,
pytest.mark.asyncio,
pytest.mark.skipif(
not SUPPORTED, reason="See pyproject.toml comment on aiohttp dependency"
),
]
pytestmark = [pytest.mark.pook]

URL = "https://httpbin.org/status/404"

class TestStandardAiohttp(StandardTests):
is_async = True

async def amake_request(self, method, url):
async with aiohttp.ClientSession(loop=self.loop) as session:
req = await session.request(method=method, url=url)
content = await req.read()
return req.status, content.decode("utf-8")


binary_file = (Path(__file__).parents[1] / "fixtures" / "nothing.bin").read_bytes()


def _pook_url():
def _pook_url(URL):
return pook.head(URL).reply(200).mock


async def test_async_with_request():
mock = _pook_url()
@pytest.fixture
def URL(httpbin):
return f"{httpbin.url}/status/404"


@pytest.mark.asyncio
async def test_async_with_request(URL):
mock = _pook_url(URL)
async with aiohttp.ClientSession() as session:
async with session.head(URL) as req:
assert req.status == 200

assert len(mock.matches) == 1


async def test_await_request():
mock = _pook_url()
@pytest.mark.asyncio
async def test_await_request(URL):
mock = _pook_url(URL)
async with aiohttp.ClientSession() as session:
req = await session.head(URL)
assert req.status == 200

assert len(mock.matches) == 1


async def test_binary_body():
@pytest.mark.asyncio
async def test_binary_body(URL):
pook.get(URL).reply(200).body(binary_file, binary=True)
async with aiohttp.ClientSession() as session:
req = await session.get(URL)
Expand Down
58 changes: 58 additions & 0 deletions tests/unit/interceptors/base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import asyncio
from typing import Optional, Tuple

import pytest

import pook


class StandardTests:
is_async: bool = False

async def amake_request(self, method: str, url: str) -> Tuple[int, Optional[str]]:
raise NotImplementedError(
"Sub-classes for async transports must implement `amake_request`"
)

def make_request(self, method: str, url: str) -> Tuple[int, Optional[str]]:
if self.is_async:
return self.loop.run_until_complete(self.amake_request(method, url))

raise NotImplementedError("Sub-classes must implement `make_request`")

@pytest.fixture(autouse=True, scope="class")
def _loop(self, request):
if self.is_async:
request.cls.loop = asyncio.new_event_loop()
yield
request.cls.loop.close()
else:
yield

@pytest.mark.pook
def test_activate_deactivate(self, httpbin):
url = f"{httpbin.url}/status/404"
pook.get(url).reply(200).body("hello from pook")

status, body = self.make_request("GET", url)

assert status == 200
assert body == "hello from pook"

pook.disable()

status, body = self.make_request("GET", url)

assert status == 404

@pytest.mark.pook(allow_pending_mocks=True)
def test_network_mode(self, httpbin):
upstream_url = f"{httpbin.url}/status/500"
mocked_url = f"{httpbin.url}/status/404"
pook.get(mocked_url).reply(200).body("hello from pook")
pook.enable_network()

# Avoid matching the mocks
status, body = self.make_request("POST", upstream_url)

assert status == 500
44 changes: 29 additions & 15 deletions tests/unit/interceptors/httpx_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,43 @@

from itertools import zip_longest


URL = "https://httpbin.org/status/404"
from tests.unit.interceptors.base import StandardTests


pytestmark = [pytest.mark.pook]


def test_sync():
class TestStandardAsyncHttpx(StandardTests):
is_async = True

async def amake_request(self, method, url):
async with httpx.AsyncClient() as client:
response = await client.request(method=method, url=url)
content = await response.aread()
return response.status_code, content.decode("utf-8")


class TestStandardSyncHttpx(StandardTests):
def make_request(self, method, url):
response = httpx.request(method=method, url=url)
content = response.read()
return response.status_code, content.decode("utf-8")


@pytest.fixture
def URL(httpbin):
return f"{httpbin.url}/status/404"


def test_sync(URL):
pook.get(URL).times(1).reply(200).body("123")

response = httpx.get(URL)

assert response.status_code == 200


async def test_async():
async def test_async(URL):
pook.get(URL).times(1).reply(200).body(b"async_body", binary=True).mock

async with httpx.AsyncClient() as client:
Expand All @@ -29,7 +50,7 @@ async def test_async():
assert (await response.aread()) == b"async_body"


def test_json():
def test_json(URL):
(
pook.post(URL)
.times(1)
Expand All @@ -44,7 +65,8 @@ def test_json():
assert response.json() == {"title": "123abc title"}


def _check_streaming_via(response_method):
@pytest.mark.parametrize("response_method", ("iter_bytes", "iter_raw"))
def test_streaming(URL, response_method):
streamed_response = b"streamed response"
pook.get(URL).times(1).reply(200).body(streamed_response).mock

Expand All @@ -55,15 +77,7 @@ def _check_streaming_via(response_method):
assert bytes().join(read_bytes) == streamed_response


def test_streaming_via_iter_bytes():
_check_streaming_via("iter_bytes")


def test_streaming_via_iter_raw():
_check_streaming_via("iter_raw")


def test_redirect_following():
def test_redirect_following(URL):
urls = [URL, f"{URL}/redirected", f"{URL}/redirected_again"]
for req, dest in zip_longest(urls, urls[1:], fillvalue=None):
if not dest:
Expand Down
6 changes: 5 additions & 1 deletion tests/unit/interceptors/module_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@


class CustomInterceptor(interceptors.BaseInterceptor):
pass
def activate(self):
...

def disable(self):
...


def test_add_custom_interceptor():
Expand Down
Loading

0 comments on commit ef5bb1a

Please sign in to comment.