From 792aea7a6c584477d77051290f02a029ef0f3725 Mon Sep 17 00:00:00 2001 From: Jamie Lennox Date: Wed, 20 Apr 2022 16:08:19 +1000 Subject: [PATCH] Allow passing json_encoder to mocking This will let people interact better with Django or similar encoders. You can set it for the whole mocker or only on individual responses. Closes: #188 --- doc/source/mocker.rst | 30 ++++++++++++++++++- .../Set-JSON-Encoder-31889bc42d11b7d3.yaml | 6 ++++ requests_mock/adapter.py | 3 ++ requests_mock/mocker.py | 2 ++ requests_mock/mocker.pyi | 15 ++++++++-- requests_mock/response.py | 13 ++++++-- tests/test_mocker.py | 25 ++++++++++++++++ 7 files changed, 89 insertions(+), 5 deletions(-) create mode 100644 releasenotes/notes/Set-JSON-Encoder-31889bc42d11b7d3.yaml diff --git a/doc/source/mocker.rst b/doc/source/mocker.rst index a1014d2..0e5ecf2 100644 --- a/doc/source/mocker.rst +++ b/doc/source/mocker.rst @@ -7,9 +7,10 @@ Using the Mocker The mocker is a loading mechanism to ensure the adapter is correctly in place to intercept calls from requests. Its goal is to provide an interface that is as close to the real requests library interface as possible. -:py:class:`requests_mock.Mocker` takes two optional parameters: +:py:class:`requests_mock.Mocker` takes optional parameters: :real_http (bool): If :py:const:`True` then any requests that are not handled by the mocking adapter will be forwarded to the real server (see :ref:`RealHTTP`), or the containing Mocker if applicable (see :ref:`NestingMockers`). Defaults to :py:const:`False`. +:json_encoder (json.JSONEncoder): If set uses the provided json encoder for all JSON responses compiled as part of the mocker. :session (requests.Session): If set, only the given session instance is mocked (see :ref:`SessionMocking`). Activation @@ -166,6 +167,33 @@ Similarly when using a mocker you can register an individual URI to bypass the m 'resp' 200 + +.. _JsonEncoder: + +JSON Encoder +============ + +In python's json module you can customize the way data is encoded by subclassing the :py:class:`~json.JSONEncoder` object and passing it to encode. +A common example of this might be to use `DjangoJSONEncoder ` for responses. + +You can specify this encoder object either when creating the :py:class:`requests_mock.Mocker` or individually at the mock creation time. + +.. doctest:: + + >>> import django.core.serializers.json.DjangoJSONEncoder as DjangoJSONEncoder + >>> with requests_mock.Mocker(json_encoder=DjangoJSONEncoder) as m: + ... m.register_uri('GET', 'http://test.com', json={'hello': 'world'}) + ... print(requests.get('http://test.com').text) + +or + +.. doctest:: + + >>> import django.core.serializers.json.DjangoJSONEncoder as DjangoJSONEncoder + >>> with requests_mock.Mocker() as m: + ... m.register_uri('GET', 'http://test.com', json={'hello': 'world'}, json_encoder=DjangoJSONEncoder) + ... print(requests.get('http://test.com').text) + .. _NestingMockers: Nested Mockers diff --git a/releasenotes/notes/Set-JSON-Encoder-31889bc42d11b7d3.yaml b/releasenotes/notes/Set-JSON-Encoder-31889bc42d11b7d3.yaml new file mode 100644 index 0000000..19d4e07 --- /dev/null +++ b/releasenotes/notes/Set-JSON-Encoder-31889bc42d11b7d3.yaml @@ -0,0 +1,6 @@ +--- +features: + - | + You can now set the JSON encoder for use by the json= parameter on either + the mocker or an individual mocked response. This will make it easier to + work with systems that encode in a specific way. diff --git a/requests_mock/adapter.py b/requests_mock/adapter.py index 20f6598..e0560b2 100644 --- a/requests_mock/adapter.py +++ b/requests_mock/adapter.py @@ -273,6 +273,7 @@ def register_uri(self, method, url, response_list=None, **kwargs): additional_matcher = kwargs.pop('additional_matcher', None) request_headers = kwargs.pop('request_headers', {}) real_http = kwargs.pop('_real_http', False) + json_encoder = kwargs.pop('json_encoder', None) if response_list and kwargs: raise RuntimeError('You should specify either a list of ' @@ -281,6 +282,8 @@ def register_uri(self, method, url, response_list=None, **kwargs): raise RuntimeError('You should specify either response data ' 'OR real_http. Not both.') elif not response_list: + if json_encoder is not None: + kwargs['json_encoder'] = json_encoder response_list = [] if real_http else [kwargs] # NOTE(jamielennox): case_sensitive is not present as a kwarg because i diff --git a/requests_mock/mocker.py b/requests_mock/mocker.py index 7da39c8..d3bc855 100644 --- a/requests_mock/mocker.py +++ b/requests_mock/mocker.py @@ -130,6 +130,7 @@ def __init__(self, session=None, **kwargs): adapter.Adapter(case_sensitive=self.case_sensitive) ) + self._json_encoder = kwargs.pop('json_encoder', None) self.real_http = kwargs.pop('real_http', False) self._last_send = None @@ -230,6 +231,7 @@ def register_uri(self, *args, **kwargs): # you can pass real_http here, but it's private to pass direct to the # adapter, because if you pass direct to the adapter you'll see the exc kwargs['_real_http'] = kwargs.pop('real_http', False) + kwargs.setdefault('json_encoder', self._json_encoder) return self._adapter.register_uri(*args, **kwargs) def request(self, *args, **kwargs): diff --git a/requests_mock/mocker.pyi b/requests_mock/mocker.pyi index 83bb34b..dc643d3 100644 --- a/requests_mock/mocker.pyi +++ b/requests_mock/mocker.pyi @@ -1,5 +1,6 @@ # Stubs for requests_mock.mocker +from json import JSONEncoder from http.cookiejar import CookieJar from io import IOBase from typing import Any, Callable, Dict, List, Optional, Pattern, Type, TypeVar, Union @@ -57,6 +58,7 @@ class MockerCore: raw: HTTPResponse = ..., exc: Union[Exception, Type[Exception]] = ..., additional_matcher: Callable[[_RequestObjectProxy], bool] = ..., + json_encoder: Optional[Type[JSONEncoder]] = ..., **kwargs: Any, ) -> _Matcher: ... @@ -79,6 +81,7 @@ class MockerCore: raw: HTTPResponse = ..., exc: Union[Exception, Type[Exception]] = ..., additional_matcher: Callable[[_RequestObjectProxy], bool] = ..., + json_encoder: Optional[Type[JSONEncoder]] = ..., **kwargs: Any, ) -> _Matcher: ... @@ -100,6 +103,7 @@ class MockerCore: raw: HTTPResponse = ..., exc: Union[Exception, Type[Exception]] = ..., additional_matcher: Callable[[_RequestObjectProxy], bool] = ..., + json_encoder: Optional[Type[JSONEncoder]] = ..., **kwargs: Any, ) -> _Matcher: ... @@ -121,6 +125,7 @@ class MockerCore: raw: HTTPResponse = ..., exc: Union[Exception, Type[Exception]] = ..., additional_matcher: Callable[[_RequestObjectProxy], bool] = ..., + json_encoder: Optional[Type[JSONEncoder]] = ..., **kwargs: Any, ) -> _Matcher: ... @@ -142,6 +147,7 @@ class MockerCore: raw: HTTPResponse = ..., exc: Union[Exception, Type[Exception]] = ..., additional_matcher: Callable[[_RequestObjectProxy], bool] = ..., + json_encoder: Optional[Type[JSONEncoder]] = ..., **kwargs: Any, ) -> _Matcher: ... @@ -163,6 +169,7 @@ class MockerCore: raw: HTTPResponse = ..., exc: Union[Exception, Type[Exception]] = ..., additional_matcher: Callable[[_RequestObjectProxy], bool] = ..., + json_encoder: Optional[Type[JSONEncoder]] = ..., **kwargs: Any, ) -> _Matcher: ... @@ -184,6 +191,7 @@ class MockerCore: raw: HTTPResponse = ..., exc: Union[Exception, Type[Exception]] = ..., additional_matcher: Callable[[_RequestObjectProxy], bool] = ..., + json_encoder: Optional[Type[JSONEncoder]] = ..., **kwargs: Any, ) -> _Matcher: ... @@ -205,6 +213,7 @@ class MockerCore: raw: HTTPResponse = ..., exc: Union[Exception, Type[Exception]] = ..., additional_matcher: Callable[[_RequestObjectProxy], bool] = ..., + json_encoder: Optional[Type[JSONEncoder]] = ..., **kwargs: Any, ) -> _Matcher: ... @@ -226,6 +235,7 @@ class MockerCore: raw: HTTPResponse = ..., exc: Union[Exception, Type[Exception]] = ..., additional_matcher: Callable[[_RequestObjectProxy], bool] = ..., + json_encoder: Optional[Type[JSONEncoder]] = ..., **kwargs: Any, ) -> _Matcher: ... @@ -241,8 +251,9 @@ class Mocker(MockerCore): case_sensitive: bool = ..., adapter: Any = ..., session: Optional[Session] = ..., - real_http: bool = ...) -> None: - ... + real_http: bool = ..., + json_encoder: Optional[Type[JSONEncoder]] = ..., + ) -> None: ... def __enter__(self) -> Any: ... def __exit__(self, type: Any, value: Any, traceback: Any) -> None: ... def __call__(self, obj: Any) -> Any: ... diff --git a/requests_mock/response.py b/requests_mock/response.py index 4219081..3643625 100644 --- a/requests_mock/response.py +++ b/requests_mock/response.py @@ -24,7 +24,13 @@ from requests_mock import exceptions _BODY_ARGS = frozenset(['raw', 'body', 'content', 'text', 'json']) -_HTTP_ARGS = frozenset(['status_code', 'reason', 'headers', 'cookies']) +_HTTP_ARGS = frozenset([ + 'status_code', + 'reason', + 'headers', + 'cookies', + 'json_encoder', +]) _DEFAULT_STATUS = 200 _http_adapter = HTTPAdapter() @@ -145,6 +151,7 @@ def create_response(request, **kwargs): :param unicode text: A text string to return upon a successful match. :param object json: A python object to be converted to a JSON string and returned upon a successful match. + :param class json_encoder: Encoder object to use for JOSON. :param dict headers: A dictionary object containing headers that are returned upon a successful match. :param CookieJar cookies: A cookie jar with cookies to set on the @@ -171,7 +178,8 @@ def create_response(request, **kwargs): raise TypeError('Text should be string data') if json is not None: - text = jsonutils.dumps(json) + encoder = kwargs.pop('json_encoder', None) or jsonutils.JSONEncoder + text = jsonutils.dumps(json, cls=encoder) if text is not None: encoding = get_encoding_from_headers(headers) or 'utf-8' content = text.encode(encoding) @@ -265,6 +273,7 @@ def _call(f, *args, **kwargs): content=_call(self._params.get('content')), body=_call(self._params.get('body')), raw=self._params.get('raw'), + json_encoder=self._params.get('json_encoder'), status_code=context.status_code, reason=context.reason, headers=context.headers, diff --git a/tests/test_mocker.py b/tests/test_mocker.py index 71ead18..8d015cd 100644 --- a/tests/test_mocker.py +++ b/tests/test_mocker.py @@ -10,6 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. +import json import pickle import mock @@ -600,3 +601,27 @@ def test_stream_zero_bytes(self, m): full_val = res.raw.read() self.assertEqual(content, full_val) + + def test_with_json_encoder_on_mocker(self): + test_val = 'hello world' + + class MyJsonEncoder(json.JSONEncoder): + def encode(s, o): + return test_val + + with requests_mock.Mocker(json_encoder=MyJsonEncoder) as m: + m.get("http://test", json={"a": "b"}) + res = requests.get("http://test") + self.assertEqual(test_val, res.text) + + @requests_mock.mock() + def test_with_json_encoder_on_endpoint(self, m): + test_val = 'hello world' + + class MyJsonEncoder(json.JSONEncoder): + def encode(s, o): + return test_val + + m.get("http://test", json={"a": "b"}, json_encoder=MyJsonEncoder) + res = requests.get("http://test") + self.assertEqual(test_val, res.text)