From a27d9d9397ef1ea5e6b0892f78ce889a80cde685 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Sousa?= Date: Tue, 6 May 2025 22:13:16 +0100 Subject: [PATCH 1/4] Add type hints to mocket.plugins.httpretty (#290) * Add type hints to mocket.plugins.httpretty * Add types-requests test dependency * Add unit test to get_mocketize --- mocket/mocket.py | 6 ++-- mocket/mocks/mockhttp.py | 2 +- mocket/plugins/httpretty/__init__.py | 48 +++++++++++++++------------- mocket/utils.py | 30 ++++++++++++++--- pyproject.toml | 12 +++++++ tests/test_mocket_utils.py | 31 ++++++++++++++++++ 6 files changed, 99 insertions(+), 30 deletions(-) create mode 100644 tests/test_mocket_utils.py diff --git a/mocket/mocket.py b/mocket/mocket.py index a01a7b46..c9e6e204 100644 --- a/mocket/mocket.py +++ b/mocket/mocket.py @@ -4,7 +4,7 @@ import itertools import os from pathlib import Path -from typing import TYPE_CHECKING, ClassVar +from typing import TYPE_CHECKING, Any, ClassVar import mocket.inject from mocket.recording import MocketRecordStorage @@ -99,12 +99,12 @@ def reset(cls) -> None: cls._record_storage = None @classmethod - def last_request(cls): + def last_request(cls) -> Any: if cls.has_requests(): return cls._requests[-1] @classmethod - def request_list(cls): + def request_list(cls) -> list[Any]: return cls._requests @classmethod diff --git a/mocket/mocks/mockhttp.py b/mocket/mocks/mockhttp.py index 245a11af..3db6a65d 100644 --- a/mocket/mocks/mockhttp.py +++ b/mocket/mocks/mockhttp.py @@ -88,7 +88,7 @@ def __init__(self, body="", status=200, headers=None): self.data = self.get_protocol_data() + self.body - def get_protocol_data(self, str_format_fun_name="capitalize"): + def get_protocol_data(self, str_format_fun_name: str = "capitalize") -> bytes: status_line = f"HTTP/1.1 {self.status} {STATUS[self.status]}" header_lines = CRLF.join( ( diff --git a/mocket/plugins/httpretty/__init__.py b/mocket/plugins/httpretty/__init__.py index fac61840..97a2c3a4 100644 --- a/mocket/plugins/httpretty/__init__.py +++ b/mocket/plugins/httpretty/__init__.py @@ -1,3 +1,5 @@ +from typing import Any, Dict, Optional + from mocket import mocketize from mocket.async_mocket import async_mocketize from mocket.compat import ENCODING @@ -7,33 +9,35 @@ from mocket.mockhttp import Response as MocketHttpResponse -def httprettifier_headers(headers): +def httprettifier_headers(headers: Dict[str, str]) -> Dict[str, str]: return {k.lower().replace("_", "-"): v for k, v in headers.items()} class Request(MocketHttpRequest): @property - def body(self): - return super().body.encode(ENCODING) + def body(self) -> bytes: + return super().body.encode(ENCODING) # type: ignore[no-any-return] @property - def headers(self): + def headers(self) -> Dict[str, str]: return httprettifier_headers(super().headers) class Response(MocketHttpResponse): - def get_protocol_data(self, str_format_fun_name="lower"): + headers: Dict[str, str] + + def get_protocol_data(self, str_format_fun_name: str = "lower") -> bytes: if "server" in self.headers and self.headers["server"] == "Python/Mocket": self.headers["server"] = "Python/HTTPretty" - return super().get_protocol_data(str_format_fun_name=str_format_fun_name) + return super().get_protocol_data(str_format_fun_name=str_format_fun_name) # type: ignore[no-any-return] - def set_base_headers(self): + def set_base_headers(self) -> None: super().set_base_headers() self.headers = httprettifier_headers(self.headers) original_set_base_headers = set_base_headers - def set_extra_headers(self, headers): + def set_extra_headers(self, headers: Dict[str, str]) -> None: self.headers.update(headers) @@ -60,17 +64,17 @@ class Entry(MocketHttpEntry): def register_uri( - method, - uri, - body="HTTPretty :)", - adding_headers=None, - forcing_headers=None, - status=200, - responses=None, - match_querystring=False, - priority=0, - **headers, -): + method: str, + uri: str, + body: str = "HTTPretty :)", + adding_headers: Optional[Dict[str, str]] = None, + forcing_headers: Optional[Dict[str, str]] = None, + status: int = 200, + responses: Any = None, + match_querystring: bool = False, + priority: int = 0, + **headers: str, +) -> None: headers = httprettifier_headers(headers) if adding_headers is not None: @@ -81,9 +85,9 @@ def register_uri( def force_headers(self): self.headers = httprettifier_headers(forcing_headers) - Response.set_base_headers = force_headers + Response.set_base_headers = force_headers # type: ignore[method-assign] else: - Response.set_base_headers = Response.original_set_base_headers + Response.set_base_headers = Response.original_set_base_headers # type: ignore[method-assign] if responses: Entry.register(method, uri, *responses) @@ -110,7 +114,7 @@ def __getattr__(self, name): HTTPretty = MocketHTTPretty() -HTTPretty.register_uri = register_uri +HTTPretty.register_uri = register_uri # type: ignore[attr-defined] httpretty = HTTPretty __all__ = ( diff --git a/mocket/utils.py b/mocket/utils.py index 60ddd9f2..6180ae3f 100644 --- a/mocket/utils.py +++ b/mocket/utils.py @@ -2,12 +2,34 @@ import binascii import contextlib -from typing import Callable +from typing import Any, Callable, Protocol, TypeVar, overload import decorator +from typing_extensions import ParamSpec from mocket.compat import decode_from_bytes, encode_to_bytes +_P = ParamSpec("_P") +_R = TypeVar("_R") + + +class MocketizeDecorator(Protocol): + """ + This is a generic decorator signature, currently applicable to get_mocketize. + + Decorators can be used as: + 1. A function that transforms func (the parameter) into func1 (the returned object). + 2. A function that takes keyword arguments and returns 1. + """ + + @overload + def __call__(self, func: Callable[_P, _R], /) -> Callable[_P, _R]: ... + + @overload + def __call__( + self, **kwargs: Any + ) -> Callable[[Callable[_P, _R]], Callable[_P, _R]]: ... + def hexdump(binary_string: bytes) -> str: r""" @@ -30,11 +52,11 @@ def hexload(string: str) -> bytes: raise ValueError from e -def get_mocketize(wrapper_: Callable) -> Callable: +def get_mocketize(wrapper_: Callable) -> MocketizeDecorator: # trying to support different versions of `decorator` with contextlib.suppress(TypeError): - return decorator.decorator(wrapper_, kwsyntax=True) # type: ignore[call-arg,unused-ignore] - return decorator.decorator(wrapper_) + return decorator.decorator(wrapper_, kwsyntax=True) # type: ignore[return-value, call-arg, unused-ignore] + return decorator.decorator(wrapper_) # type: ignore[return-value] __all__ = ( diff --git a/pyproject.toml b/pyproject.toml index 6872741f..b8631517 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -57,6 +57,7 @@ test = [ "wait-for-it", "mypy", "types-decorator", + "types-requests", ] speedups = [ "xxhash;platform_python_implementation=='CPython'", @@ -123,6 +124,9 @@ files = [ "mocket/exceptions.py", "mocket/compat.py", "mocket/utils.py", + "mocket/plugins/httpretty/__init__.py", + "tests/test_httpretty.py", + "tests/test_mocket_utils.py", # "tests/" ] strict = true @@ -140,3 +144,11 @@ disable_error_code = ["no-untyped-def"] # enable this once full type-coverage is [[tool.mypy.overrides]] module = "tests.*" disable_error_code = ['type-arg', 'no-untyped-def'] + +[[tool.mypy.overrides]] +module = "mocket.plugins.*" +disallow_subclassing_any = false # mypy doesn't support dynamic imports + +[[tool.mypy.overrides]] +module = "tests.test_httpretty" +disallow_untyped_decorators = true diff --git a/tests/test_mocket_utils.py b/tests/test_mocket_utils.py new file mode 100644 index 00000000..d3b5eba7 --- /dev/null +++ b/tests/test_mocket_utils.py @@ -0,0 +1,31 @@ +from typing import Callable +from unittest import TestCase +from unittest.mock import NonCallableMock, patch + +import decorator + +from mocket.utils import get_mocketize + + +def mock_decorator(func: Callable[[], None]) -> None: + return func() + + +class GetMocketizeTestCase(TestCase): + @patch.object(decorator, "decorator") + def test_get_mocketize_with_kwsyntax(self, dec: NonCallableMock) -> None: + get_mocketize(mock_decorator) + dec.assert_called_once_with(mock_decorator, kwsyntax=True) + + @patch.object(decorator, "decorator") + def test_get_mocketize_without_kwsyntax(self, dec: NonCallableMock) -> None: + dec.side_effect = [ + TypeError("kwsyntax is not supported in this version of decorator"), + mock_decorator, + ] + + get_mocketize(mock_decorator) + # First time called with kwsyntax=True, which failed with TypeError + dec.call_args_list[0].assert_compare_to((mock_decorator,), {"kwsyntax": True}) + # Second time without kwsyntax, which succeeds + dec.call_args_list[1].assert_compare_to((mock_decorator,)) From 663d053073ae426ece20d5397decaea0b067dd65 Mon Sep 17 00:00:00 2001 From: Giorgio Salluzzo Date: Tue, 6 May 2025 23:47:43 +0200 Subject: [PATCH 2/4] Renaming test file. --- pyproject.toml | 2 +- tests/{test_mocket_utils.py => test_utils.py} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename tests/{test_mocket_utils.py => test_utils.py} (100%) diff --git a/pyproject.toml b/pyproject.toml index b8631517..09c50435 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -126,7 +126,7 @@ files = [ "mocket/utils.py", "mocket/plugins/httpretty/__init__.py", "tests/test_httpretty.py", - "tests/test_mocket_utils.py", + "tests/test_utils.py", # "tests/" ] strict = true diff --git a/tests/test_mocket_utils.py b/tests/test_utils.py similarity index 100% rename from tests/test_mocket_utils.py rename to tests/test_utils.py From 9c7ea5ecdcb5269092078bdc29a9d1bc1e86561d Mon Sep 17 00:00:00 2001 From: Giorgio Salluzzo Date: Sun, 18 May 2025 10:15:40 +0200 Subject: [PATCH 3/4] Update pyproject.toml --- pyproject.toml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 09c50435..5349ad6c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,7 +9,6 @@ description = "Socket Mock Framework - for all kinds of socket animals, web-clie readme = { file = "README.rst", content-type = "text/x-rst" } license = { file = "LICENSE" } authors = [{ name = "Giorgio Salluzzo", email = "giorgio.salluzzo@gmail.com" }] -urls = { github = "https://github.com/mindflayer/python-mocket" } classifiers = [ "Development Status :: 6 - Mature", "Intended Audience :: Developers", @@ -35,6 +34,10 @@ dependencies = [ ] dynamic = ["version"] +[project.urls] +Homepage = "https://github.com/mindflayer/python-mocket" +Repository = "https://github.com/mindflayer/python-mocket" + [project.optional-dependencies] test = [ "pre-commit", From 80e10479d0b382f60814beb5216915e3c7c0fc42 Mon Sep 17 00:00:00 2001 From: Giorgio Salluzzo Date: Sun, 18 May 2025 10:16:18 +0200 Subject: [PATCH 4/4] Bump version --- mocket/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mocket/__init__.py b/mocket/__init__.py index cd9437e9..e4bc0084 100644 --- a/mocket/__init__.py +++ b/mocket/__init__.py @@ -31,4 +31,4 @@ "FakeSSLContext", ) -__version__ = "3.13.5" +__version__ = "3.13.6" pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

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:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy