diff --git a/.github/workflows/ci_cd.yml b/.github/workflows/ci_cd.yml index eb2f06f..8b8501c 100644 --- a/.github/workflows/ci_cd.yml +++ b/.github/workflows/ci_cd.yml @@ -32,7 +32,7 @@ jobs: run: pipx install hatch - name: Lint - run: hatch run lint:run + run: hatch run lint build: name: build diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 652e077..68baea6 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -26,3 +26,11 @@ repos: - id: ruff args: [--fix] - id: ruff-format + + - repo: local + hooks: + - id: types + name: types + entry: hatch run types + language: system + pass_filenames: false diff --git a/README.rst b/README.rst index 9969fab..26f7fae 100644 --- a/README.rst +++ b/README.rst @@ -261,14 +261,14 @@ Install the pre-commit hook: .. code:: bash - hatch run lint:install + hatch run lint-install Lint the code: .. code:: bash - hatch run lint:run + hatch run lint Run tests on all supported Python versions and implementations (this requires your host operating system to have each implementation available): diff --git a/pyproject.toml b/pyproject.toml index f6ab255..bfea186 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -51,8 +51,10 @@ packages = ["src/pook"] [tool.hatch.envs.default] python = "3.12" -scripts = { test = 'pytest {args}' } extra-dependencies = [ + "pre-commit~=4.0", + "mypy>=1.11.2", + "pytest~=8.3", "pytest-asyncio~=0.24", "pytest-pook==0.1.0b0", @@ -69,14 +71,12 @@ extra-dependencies = [ "mocket[pook]~=3.12.2; platform_python_implementation != 'PyPy'", ] -[tool.hatch.envs.lint] -extra-dependencies = [ - "pre-commit~=4.0", -] +[tool.hatch.envs.default.scripts] +test = "pytest {args}" +types = "mypy --install-types --non-interactive src/pook/interceptors {args}" -[tool.hatch.envs.lint.scripts] -install = 'pre-commit install' -run = 'pre-commit run --all-files' +lint-install = "pre-commit install" +lint = "pre-commit run --all-files" [tool.hatch.envs.docs] extra-dependencies = [ @@ -85,9 +85,9 @@ extra-dependencies = [ ] [tool.hatch.envs.docs.scripts] -apidocs = 'sphinx-apidoc -f --follow-links -H "API documentation" -o docs/source src/pook' -htmldocs = 'rm -rf docs/_build && sphinx-build -b html -d docs/_build/doctrees ./docs docs/_build/html' -build = 'hatch run apidocs; hatch run htmldocs' +apidocs = "sphinx-apidoc -f --follow-links -H 'API documentation' -o docs/source src/pook" +htmldocs = "rm -rf docs/_build && sphinx-build -b html -d docs/_build/doctrees ./docs docs/_build/html" +build = "hatch run apidocs; hatch run htmldocs" [tool.hatch.envs.test] [[tool.hatch.envs.test.matrix]] diff --git a/src/pook/__init__.py b/src/pook/__init__.py index 4e0a417..1745f5c 100644 --- a/src/pook/__init__.py +++ b/src/pook/__init__.py @@ -1,8 +1,43 @@ -from .api import * # noqa -from .api import __all__ as api_exports +from .api import * # noqa: F403 # Delegate to API export -__all__ = api_exports +__all__ = ( + "activate", # noqa: F405 + "on", # noqa: F405 + "disable", # noqa: F405 + "off", # noqa: F405 + "reset", # noqa: F405 + "engine", # noqa: F405 + "use_network", # noqa: F405 + "enable_network", # noqa: F405 + "disable_network", # noqa: F405 + "get", # noqa: F405 + "post", # noqa: F405 + "put", # noqa: F405 + "patch", # noqa: F405 + "head", # noqa: F405 + "use", # noqa: F405 + "set_mock_engine", # noqa: F405 + "delete", # noqa: F405 + "options", # noqa: F405 + "pending", # noqa: F405 + "ispending", # noqa: F405 + "mock", # noqa: F405 + "pending_mocks", # noqa: F405 + "unmatched_requests", # noqa: F405 + "isunmatched", # noqa: F405 + "unmatched", # noqa: F405 + "isactive", # noqa: F405 + "isdone", # noqa: F405 + "regex", # noqa: F405 + "Engine", # noqa: F405 + "Mock", # noqa: F405 + "Request", # noqa: F405 + "Response", # noqa: F405 + "MatcherEngine", # noqa: F405 + "MockEngine", # noqa: F405 + "use_network_filter", # noqa: F405 +) # Package metadata __author__ = "Tomas Aparicio" diff --git a/src/pook/interceptors/_httpx.py b/src/pook/interceptors/_httpx.py index 92e3040..07566ae 100644 --- a/src/pook/interceptors/_httpx.py +++ b/src/pook/interceptors/_httpx.py @@ -1,11 +1,14 @@ import asyncio from http.client import responses as http_reasons from unittest import mock +import typing as t import httpx -from ..request import Request -from .base import BaseInterceptor +from pook.request import Request # type: ignore +from pook.response import Response # type: ignore +from pook.interceptors.base import BaseInterceptor + PATCHES = ( "httpx.Client._transport_for_url", @@ -13,6 +16,12 @@ ) +HttpxClient = t.Union[httpx.Client, httpx.AsyncClient] +TransportForUrl = t.Callable[ + [HttpxClient, httpx.URL], t.Union[httpx.BaseTransport, httpx.AsyncBaseTransport] +] + + class HttpxInterceptor(BaseInterceptor): """ httpx client traffic interceptor. @@ -31,7 +40,7 @@ def handler(client, *_): try: patcher = mock.patch(path, handler) - _original_transport_for_url = patcher.get_original()[0] + _original_transport_for_url = patcher.get_original()[0] # type: ignore[var-annotated] patcher.start() except Exception: pass @@ -45,20 +54,32 @@ def disable(self): [patch.stop() for patch in self.patchers] -class MockedTransport(httpx.BaseTransport): - def __init__(self, interceptor, client, _original_transport_for_url): +T = t.TypeVar("T", httpx.BaseTransport, httpx.AsyncBaseTransport) + + +class MockedTransport(httpx.BaseTransport, t.Generic[T]): + _original_transport_for_url: t.Callable[[HttpxClient, httpx.URL], T] + + def __init__( + self, + interceptor: HttpxInterceptor, + client: HttpxClient, + _original_transport_for_url: t.Callable[[HttpxClient, httpx.URL], T], + ): self._interceptor = interceptor self._client = client self._original_transport_for_url = _original_transport_for_url - def _get_pook_request(self, httpx_request): + def _get_pook_request(self, httpx_request: httpx.Request) -> Request: req = Request(httpx_request.method) req.url = str(httpx_request.url) req.headers = httpx_request.headers return req - def _get_httpx_response(self, httpx_request, mock_response): + def _get_httpx_response( + self, httpx_request: httpx.Request, mock_response: Response + ) -> httpx.Response: res = httpx.Response( status_code=mock_response._status, headers=mock_response._headers, @@ -66,7 +87,7 @@ def _get_httpx_response(self, httpx_request, mock_response): extensions={ # TODO: Add HTTP2 response support "http_version": b"HTTP/1.1", - "reason_phrase": http_reasons.get(mock_response._status).encode( + "reason_phrase": http_reasons.get(mock_response._status, "").encode( "ascii" ), "network_stream": None, @@ -83,7 +104,7 @@ def _get_httpx_response(self, httpx_request, mock_response): return res -class AsyncTransport(MockedTransport): +class AsyncTransport(MockedTransport[httpx.AsyncBaseTransport]): async def _get_pook_request(self, httpx_request): req = super()._get_pook_request(httpx_request) req.body = await httpx_request.aread() @@ -104,7 +125,7 @@ async def handle_async_request(self, request): return self._get_httpx_response(request, mock._response) -class SyncTransport(MockedTransport): +class SyncTransport(MockedTransport[httpx.BaseTransport]): def _get_pook_request(self, httpx_request): req = super()._get_pook_request(httpx_request) req.body = httpx_request.read() diff --git a/src/pook/interceptors/aiohttp.py b/src/pook/interceptors/aiohttp.py index 9bdc66e..f36ef49 100644 --- a/src/pook/interceptors/aiohttp.py +++ b/src/pook/interceptors/aiohttp.py @@ -6,15 +6,12 @@ from aiohttp.helpers import TimerNoop from aiohttp.streams import EmptyStreamReader -from ..request import Request -from .base import BaseInterceptor +from pook.request import Request # type: ignore +from pook.interceptors.base import BaseInterceptor # Try to load yarl URL parser package used by aiohttp -try: - import multidict - import yarl -except Exception: - yarl, multidict = None, None +import multidict +import yarl PATCHES = ("aiohttp.client.ClientSession._request",) diff --git a/src/pook/interceptors/http.py b/src/pook/interceptors/http.py index 7a67d17..3ba20da 100644 --- a/src/pook/interceptors/http.py +++ b/src/pook/interceptors/http.py @@ -1,15 +1,14 @@ import socket -from http.client import ( - _CS_REQ_SENT, - HTTPSConnection, -) +from http.client import _CS_REQ_SENT # type: ignore[attr-defined] +from http.client import HTTPSConnection + from http.client import ( responses as http_reasons, ) from unittest import mock -from ..request import Request -from .base import BaseInterceptor +from pook.request import Request # type: ignore +from pook.interceptors.base import BaseInterceptor PATCHES = ("http.client.HTTPConnection.request",) @@ -88,8 +87,8 @@ def getresponse(): conn.getresponse = getresponse - conn.__response = mockres - conn.__state = _CS_REQ_SENT + conn.__response = mockres # type: ignore[attr-defined] + conn.__state = _CS_REQ_SENT # type: ignore[attr-defined] # Path reader def read(): diff --git a/src/pook/interceptors/urllib3.py b/src/pook/interceptors/urllib3.py index 9c81f9b..42c1fac 100644 --- a/src/pook/interceptors/urllib3.py +++ b/src/pook/interceptors/urllib3.py @@ -7,9 +7,9 @@ ) from unittest import mock -from ..request import Request -from .base import BaseInterceptor -from .http import URLLIB3_BYPASS +from pook.request import Request # type: ignore +from pook.interceptors.base import BaseInterceptor +from pook.interceptors.http import URLLIB3_BYPASS PATCHES = ( "requests.packages.urllib3.connectionpool.HTTPConnectionPool.urlopen", @@ -28,7 +28,7 @@ def HTTPResponse(path, *args, **kw): # Infer package package = path.split(".").pop(0) # Get import path - import_path = RESPONSE_PATH.get(package) + import_path = RESPONSE_PATH[package] # Dynamically load package module = __import__(import_path, fromlist=(RESPONSE_CLASS,)) @@ -148,8 +148,8 @@ def _on_request( if is_chunked_response(headers): body_chunks = body if isinstance(body, list) else [body] - body = ClientHTTPResponse(MockSock) - body.fp = FakeChunkedResponseBody(body_chunks) + body = ClientHTTPResponse(MockSock) # type: ignore + body.fp = FakeChunkedResponseBody(body_chunks) # type:ignore else: # Assume that the body is a bytes-like object body = io.BytesIO(res._body) diff --git a/src/pook/request.py b/src/pook/request.py index 82394e6..6868994 100644 --- a/src/pook/request.py +++ b/src/pook/request.py @@ -77,10 +77,6 @@ def extra(self, extra): def url(self): return self._url - @property - def rawurl(self): - return self._url if isregex(self._url) else urlunparse(self._url) - @url.setter def url(self, url): if isregex(url): @@ -96,6 +92,10 @@ def url(self, url): else self._query ) + @property + def rawurl(self): + return self._url if isregex(self._url) else urlunparse(self._url) + @property def query(self): return self._query