diff --git a/Makefile b/Makefile index f344591f..62c52dc7 100644 --- a/Makefile +++ b/Makefile @@ -30,7 +30,9 @@ types: test: types @echo "Running Python tests" + uv pip uninstall pook || true export VIRTUAL_ENV=.venv; .venv/bin/wait-for-it --service httpbin.local:443 --service localhost:6379 --timeout 5 -- .venv/bin/pytest + uv pip install pook && .venv/bin/pytest tests/test_pook.py && uv pip uninstall pook @echo "" safetest: @@ -41,7 +43,7 @@ publish: clean install-test-requirements uv run twine upload --repository mocket dist/*.tar.gz clean: - rm -rf *.egg-info dist/ requirements.txt uv.lock || true + rm -rf .coverage *.egg-info dist/ requirements.txt uv.lock || true find . -type d -name __pycache__ -exec rm -rf {} \; || true .PHONY: clean publish safetest test setup develop lint-python test-python _services-up diff --git a/mocket/__init__.py b/mocket/__init__.py index 31cd56fc..fb0434e9 100644 --- a/mocket/__init__.py +++ b/mocket/__init__.py @@ -1,6 +1,13 @@ from .async_mocket import async_mocketize -from .mocket import Mocket, MocketEntry, Mocketizer, mocketize +from .mocket import FakeSSLContext, Mocket, MocketEntry, Mocketizer, mocketize -__all__ = ("async_mocketize", "mocketize", "Mocket", "MocketEntry", "Mocketizer") +__all__ = ( + "async_mocketize", + "mocketize", + "Mocket", + "MocketEntry", + "Mocketizer", + "FakeSSLContext", +) -__version__ = "3.13.1" +__version__ = "3.13.2" diff --git a/mocket/mocket.py b/mocket/mocket.py index daa0e608..dcdab533 100644 --- a/mocket/mocket.py +++ b/mocket/mocket.py @@ -25,7 +25,6 @@ from .compat import basestring, byte_type, decode_from_bytes, encode_to_bytes, text_type from .utils import ( - SSL_PROTOCOL, MocketMode, MocketSocketCore, get_mocketize, @@ -48,14 +47,6 @@ except ImportError: pyopenssl_override = False -try: # pragma: no cover - from aiohttp import TCPConnector - - aiohttp_make_ssl_context_cache_clear = TCPConnector._make_ssl_context.cache_clear -except (ImportError, AttributeError): - aiohttp_make_ssl_context_cache_clear = None - - true_socket = socket.socket true_create_connection = socket.create_connection true_gethostbyname = socket.gethostbyname @@ -106,20 +97,9 @@ def check_hostname(self): def check_hostname(self, _): self._check_hostname = False - def __init__(self, sock=None, server_hostname=None, _context=None, *args, **kwargs): + def __init__(self, *args, **kwargs): self._set_dummy_methods() - if isinstance(sock, MocketSocket): - self.sock = sock - self.sock._host = server_hostname - self.sock.true_socket = true_ssl_socket( - sock=self.sock.true_socket, - server_hostname=server_hostname, - _context=true_ssl_context(protocol=SSL_PROTOCOL), - ) - elif isinstance(sock, int) and true_ssl_context: - self.context = true_ssl_context(sock) - def _set_dummy_methods(self): def dummy_method(*args, **kwargs): pass @@ -128,7 +108,7 @@ def dummy_method(*args, **kwargs): setattr(self, m, dummy_method) @staticmethod - def wrap_socket(sock=sock, *args, **kwargs): + def wrap_socket(sock, *args, **kwargs): sock.kwargs = kwargs sock._secure_socket = True return sock @@ -139,10 +119,6 @@ def wrap_bio(incoming, outcoming, *args, **kwargs): ssl_obj._host = kwargs["server_hostname"] return ssl_obj - def __getattr__(self, name): - if self.sock is not None: - return getattr(self.sock, name) - def create_connection(address, timeout=None, source_address=None): s = socket.socket(socket.AF_INET, socket.SOCK_STREAM, socket.IPPROTO_TCP) @@ -394,18 +370,15 @@ def true_sendall(self, data, *args, **kwargs): self.true_socket.connect((host, port)) self.true_socket.sendall(data, *args, **kwargs) encoded_response = b"" - # https://github.com/kennethreitz/requests/blob/master/tests/testserver/server.py#L13 + # https://github.com/kennethreitz/requests/blob/master/tests/testserver/server.py#L12 while True: - if ( - not select.select([self.true_socket], [], [], 0.1)[0] - and encoded_response - ): + more_to_read = select.select([self.true_socket], [], [], 0.1)[0] + if not more_to_read and encoded_response: break - recv = self.true_socket.recv(self._buflen) - - if not recv and encoded_response: + new_content = self.true_socket.recv(self._buflen) + if not new_content: break - encoded_response += recv + encoded_response += new_content # dump the resulting dictionary to a JSON file if Mocket.get_truesocket_recording_dir(): @@ -566,8 +539,6 @@ def enable(namespace=None, truesocket_recording_dir=None): if pyopenssl_override: # pragma: no cover # Take out the pyopenssl version - use the default implementation extract_from_urllib3() - if aiohttp_make_ssl_context_cache_clear: # pragma: no cover - aiohttp_make_ssl_context_cache_clear() @staticmethod def disable(): @@ -604,8 +575,6 @@ def disable(): if pyopenssl_override: # pragma: no cover # Put the pyopenssl version back in place inject_into_urllib3() - if aiohttp_make_ssl_context_cache_clear: # pragma: no cover - aiohttp_make_ssl_context_cache_clear() @classmethod def get_namespace(cls): diff --git a/mocket/plugins/aiohttp_connector.py b/mocket/plugins/aiohttp_connector.py new file mode 100644 index 00000000..353c3af7 --- /dev/null +++ b/mocket/plugins/aiohttp_connector.py @@ -0,0 +1,18 @@ +import contextlib + +from mocket import FakeSSLContext + +with contextlib.suppress(ModuleNotFoundError): + from aiohttp import ClientRequest + from aiohttp.connector import TCPConnector + + class MocketTCPConnector(TCPConnector): + """ + `aiohttp` reuses SSLContext instances created at import-time, + making it more difficult for Mocket to do its job. + This is an attempt to make things smoother, at the cost of + slightly patching the `ClientSession` while testing. + """ + + def _get_ssl_context(self, req: ClientRequest) -> FakeSSLContext: + return FakeSSLContext() diff --git a/mocket/plugins/pook_mock_engine.py b/mocket/plugins/pook_mock_engine.py index 99cb07ec..549f5509 100644 --- a/mocket/plugins/pook_mock_engine.py +++ b/mocket/plugins/pook_mock_engine.py @@ -1,5 +1,7 @@ -from pook.engine import MockEngine -from pook.interceptors.base import BaseInterceptor +try: + from pook.engine import MockEngine +except ModuleNotFoundError: + MockEngine = object from mocket.mocket import Mocket from mocket.mockhttp import Entry, Response @@ -37,17 +39,6 @@ def single_register( return entry -class MocketInterceptor(BaseInterceptor): - @staticmethod - def activate(): - Mocket.disable() - Mocket.enable() - - @staticmethod - def disable(): - Mocket.disable() - - class MocketEngine(MockEngine): def __init__(self, engine): def mocket_mock_fun(*args, **kwargs): @@ -68,6 +59,18 @@ def mocket_mock_fun(*args, **kwargs): return mock + from pook.interceptors.base import BaseInterceptor + + class MocketInterceptor(BaseInterceptor): + @staticmethod + def activate(): + Mocket.disable() + Mocket.enable() + + @staticmethod + def disable(): + Mocket.disable() + # Store plugins engine self.engine = engine # Store HTTP client interceptors diff --git a/pyproject.toml b/pyproject.toml index e3b7d866..77d1f5d4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -46,7 +46,6 @@ test = [ "redis", "gevent", "sure", - "pook", "flake8>5", "xxhash", "httpx", @@ -54,7 +53,7 @@ test = [ "build", "twine", "fastapi", - "aiohttp<3.10.6", + "aiohttp", "wait-for-it", "mypy", "types-decorator", @@ -89,7 +88,7 @@ exclude = [ testpaths = [ "tests", "mocket", ] -addopts = "--doctest-modules --cov=mocket --cov-report=term-missing -v -x" +addopts = "--doctest-modules --cov=mocket --cov-report=term-missing --cov-append -v -x" [tool.ruff] src = ["mocket", "tests"] diff --git a/tests/test_asyncio.py b/tests/test_asyncio.py index 0f9a7d17..bef53009 100644 --- a/tests/test_asyncio.py +++ b/tests/test_asyncio.py @@ -9,6 +9,7 @@ from mocket import Mocketizer, async_mocketize from mocket.mockhttp import Entry +from mocket.plugins.aiohttp_connector import MocketTCPConnector def test_asyncio_record_replay(event_loop): @@ -46,6 +47,11 @@ async def test_asyncio_connection(): @pytest.mark.asyncio @async_mocketize async def test_aiohttp(): + """ + The alternative to using the custom `connector` would be importing + `aiohttp` when Mocket is already in control (inside the decorated test). + """ + url = "https://bar.foo/" data = {"message": "Hello"} @@ -57,7 +63,7 @@ async def test_aiohttp(): ) async with aiohttp.ClientSession( - timeout=aiohttp.ClientTimeout(total=3) + timeout=aiohttp.ClientTimeout(total=3), connector=MocketTCPConnector() ) as session, session.get(url) as response: response = await response.json() assert response == data diff --git a/tests/test_pook.py b/tests/test_pook.py index f398672e..56721b5f 100644 --- a/tests/test_pook.py +++ b/tests/test_pook.py @@ -1,29 +1,31 @@ -import pook -import requests +import contextlib -from mocket.plugins.pook_mock_engine import MocketEngine +with contextlib.suppress(ModuleNotFoundError): + import pook + import requests -pook.set_mock_engine(MocketEngine) + from mocket.plugins.pook_mock_engine import MocketEngine + pook.set_mock_engine(MocketEngine) -@pook.on -def test_pook_engine(): - url = "http://twitter.com/api/1/foobar" - status = 404 - response_json = {"error": "foo"} + @pook.on + def test_pook_engine(): + url = "http://twitter.com/api/1/foobar" + status = 404 + response_json = {"error": "foo"} - mock = pook.get( - url, - headers={"content-type": "application/json"}, - reply=status, - response_json=response_json, - ) - mock.persist() + mock = pook.get( + url, + headers={"content-type": "application/json"}, + reply=status, + response_json=response_json, + ) + mock.persist() - requests.get(url) - assert mock.calls == 1 + requests.get(url) + assert mock.calls == 1 - resp = requests.get(url) - assert resp.status_code == status - assert resp.json() == response_json - assert mock.calls == 2 + resp = requests.get(url) + assert resp.status_code == status + assert resp.json() == response_json + assert mock.calls == 2
Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.
Alternative Proxies: