Skip to content

Commit

Permalink
Allow passing json_encoder to mocking
Browse files Browse the repository at this point in the history
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
  • Loading branch information
jamielennox committed Aug 30, 2022
1 parent 27688f9 commit 792aea7
Show file tree
Hide file tree
Showing 7 changed files with 89 additions and 5 deletions.
30 changes: 29 additions & 1 deletion doc/source/mocker.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 <https://docs.djangoproject.com/en/3.2/topics/serialization/#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
Expand Down
6 changes: 6 additions & 0 deletions releasenotes/notes/Set-JSON-Encoder-31889bc42d11b7d3.yaml
Original file line number Diff line number Diff line change
@@ -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.
3 changes: 3 additions & 0 deletions requests_mock/adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 '
Expand All @@ -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
Expand Down
2 changes: 2 additions & 0 deletions requests_mock/mocker.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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):
Expand Down
15 changes: 13 additions & 2 deletions requests_mock/mocker.pyi
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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: ...

Expand All @@ -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: ...

Expand All @@ -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: ...

Expand All @@ -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: ...

Expand All @@ -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: ...

Expand All @@ -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: ...

Expand All @@ -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: ...

Expand All @@ -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: ...

Expand All @@ -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: ...

Expand All @@ -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: ...
Expand Down
13 changes: 11 additions & 2 deletions requests_mock/response.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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
Expand All @@ -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)
Expand Down Expand Up @@ -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,
Expand Down
25 changes: 25 additions & 0 deletions tests/test_mocker.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
# License for the specific language governing permissions and limitations
# under the License.

import json
import pickle

import mock
Expand Down Expand Up @@ -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)

0 comments on commit 792aea7

Please sign in to comment.