Skip to content

Commit

Permalink
Add baseline for types (#143)
Browse files Browse the repository at this point in the history
* Add mypy dependency and types script

* Add mypy for type checking

* Use explicit __all__ for main module export

* Add types to inteceptors

Moving the Request::url setter was required to work around a strange mypy bug where it did not think the property was settable if the setter was not defined immediately after the original property declaration
  • Loading branch information
sarayourfriend authored Oct 11, 2024
1 parent 34ac39e commit 2d7c699
Show file tree
Hide file tree
Showing 10 changed files with 112 additions and 52 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/ci_cd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ jobs:
run: pipx install hatch

- name: Lint
run: hatch run lint:run
run: hatch run lint

build:
name: build
Expand Down
8 changes: 8 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
4 changes: 2 additions & 2 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
22 changes: 11 additions & 11 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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 = [
Expand All @@ -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]]
Expand Down
41 changes: 38 additions & 3 deletions src/pook/__init__.py
Original file line number Diff line number Diff line change
@@ -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"
Expand Down
41 changes: 31 additions & 10 deletions src/pook/interceptors/_httpx.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,27 @@
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",
"httpx.AsyncClient._transport_for_url",
)


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.
Expand All @@ -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
Expand All @@ -45,28 +54,40 @@ 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,
content=mock_response._body,
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,
Expand All @@ -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()
Expand All @@ -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()
Expand Down
11 changes: 4 additions & 7 deletions src/pook/interceptors/aiohttp.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",)

Expand Down
15 changes: 7 additions & 8 deletions src/pook/interceptors/http.py
Original file line number Diff line number Diff line change
@@ -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",)

Expand Down Expand Up @@ -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():
Expand Down
12 changes: 6 additions & 6 deletions src/pook/interceptors/urllib3.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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,))
Expand Down Expand Up @@ -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)
Expand Down
8 changes: 4 additions & 4 deletions src/pook/request.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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
Expand Down

0 comments on commit 2d7c699

Please sign in to comment.