Skip to content

Commit 792aea7

Browse files
committed
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
1 parent 27688f9 commit 792aea7

File tree

7 files changed

+89
-5
lines changed

7 files changed

+89
-5
lines changed

doc/source/mocker.rst

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,10 @@ Using the Mocker
77
The mocker is a loading mechanism to ensure the adapter is correctly in place to intercept calls from requests.
88
Its goal is to provide an interface that is as close to the real requests library interface as possible.
99

10-
:py:class:`requests_mock.Mocker` takes two optional parameters:
10+
:py:class:`requests_mock.Mocker` takes optional parameters:
1111

1212
: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`.
13+
:json_encoder (json.JSONEncoder): If set uses the provided json encoder for all JSON responses compiled as part of the mocker.
1314
:session (requests.Session): If set, only the given session instance is mocked (see :ref:`SessionMocking`).
1415

1516
Activation
@@ -166,6 +167,33 @@ Similarly when using a mocker you can register an individual URI to bypass the m
166167
'resp'
167168
200
168169

170+
171+
.. _JsonEncoder:
172+
173+
JSON Encoder
174+
============
175+
176+
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.
177+
A common example of this might be to use `DjangoJSONEncoder <https://docs.djangoproject.com/en/3.2/topics/serialization/#djangojsonencoder>` for responses.
178+
179+
You can specify this encoder object either when creating the :py:class:`requests_mock.Mocker` or individually at the mock creation time.
180+
181+
.. doctest::
182+
183+
>>> import django.core.serializers.json.DjangoJSONEncoder as DjangoJSONEncoder
184+
>>> with requests_mock.Mocker(json_encoder=DjangoJSONEncoder) as m:
185+
... m.register_uri('GET', 'http://test.com', json={'hello': 'world'})
186+
... print(requests.get('http://test.com').text)
187+
188+
or
189+
190+
.. doctest::
191+
192+
>>> import django.core.serializers.json.DjangoJSONEncoder as DjangoJSONEncoder
193+
>>> with requests_mock.Mocker() as m:
194+
... m.register_uri('GET', 'http://test.com', json={'hello': 'world'}, json_encoder=DjangoJSONEncoder)
195+
... print(requests.get('http://test.com').text)
196+
169197
.. _NestingMockers:
170198

171199
Nested Mockers
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
features:
3+
- |
4+
You can now set the JSON encoder for use by the json= parameter on either
5+
the mocker or an individual mocked response. This will make it easier to
6+
work with systems that encode in a specific way.

requests_mock/adapter.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -273,6 +273,7 @@ def register_uri(self, method, url, response_list=None, **kwargs):
273273
additional_matcher = kwargs.pop('additional_matcher', None)
274274
request_headers = kwargs.pop('request_headers', {})
275275
real_http = kwargs.pop('_real_http', False)
276+
json_encoder = kwargs.pop('json_encoder', None)
276277

277278
if response_list and kwargs:
278279
raise RuntimeError('You should specify either a list of '
@@ -281,6 +282,8 @@ def register_uri(self, method, url, response_list=None, **kwargs):
281282
raise RuntimeError('You should specify either response data '
282283
'OR real_http. Not both.')
283284
elif not response_list:
285+
if json_encoder is not None:
286+
kwargs['json_encoder'] = json_encoder
284287
response_list = [] if real_http else [kwargs]
285288

286289
# NOTE(jamielennox): case_sensitive is not present as a kwarg because i

requests_mock/mocker.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,7 @@ def __init__(self, session=None, **kwargs):
130130
adapter.Adapter(case_sensitive=self.case_sensitive)
131131
)
132132

133+
self._json_encoder = kwargs.pop('json_encoder', None)
133134
self.real_http = kwargs.pop('real_http', False)
134135
self._last_send = None
135136

@@ -230,6 +231,7 @@ def register_uri(self, *args, **kwargs):
230231
# you can pass real_http here, but it's private to pass direct to the
231232
# adapter, because if you pass direct to the adapter you'll see the exc
232233
kwargs['_real_http'] = kwargs.pop('real_http', False)
234+
kwargs.setdefault('json_encoder', self._json_encoder)
233235
return self._adapter.register_uri(*args, **kwargs)
234236

235237
def request(self, *args, **kwargs):

requests_mock/mocker.pyi

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# Stubs for requests_mock.mocker
22

3+
from json import JSONEncoder
34
from http.cookiejar import CookieJar
45
from io import IOBase
56
from typing import Any, Callable, Dict, List, Optional, Pattern, Type, TypeVar, Union
@@ -57,6 +58,7 @@ class MockerCore:
5758
raw: HTTPResponse = ...,
5859
exc: Union[Exception, Type[Exception]] = ...,
5960
additional_matcher: Callable[[_RequestObjectProxy], bool] = ...,
61+
json_encoder: Optional[Type[JSONEncoder]] = ...,
6062
**kwargs: Any,
6163
) -> _Matcher: ...
6264

@@ -79,6 +81,7 @@ class MockerCore:
7981
raw: HTTPResponse = ...,
8082
exc: Union[Exception, Type[Exception]] = ...,
8183
additional_matcher: Callable[[_RequestObjectProxy], bool] = ...,
84+
json_encoder: Optional[Type[JSONEncoder]] = ...,
8285
**kwargs: Any,
8386
) -> _Matcher: ...
8487

@@ -100,6 +103,7 @@ class MockerCore:
100103
raw: HTTPResponse = ...,
101104
exc: Union[Exception, Type[Exception]] = ...,
102105
additional_matcher: Callable[[_RequestObjectProxy], bool] = ...,
106+
json_encoder: Optional[Type[JSONEncoder]] = ...,
103107
**kwargs: Any,
104108
) -> _Matcher: ...
105109

@@ -121,6 +125,7 @@ class MockerCore:
121125
raw: HTTPResponse = ...,
122126
exc: Union[Exception, Type[Exception]] = ...,
123127
additional_matcher: Callable[[_RequestObjectProxy], bool] = ...,
128+
json_encoder: Optional[Type[JSONEncoder]] = ...,
124129
**kwargs: Any,
125130
) -> _Matcher: ...
126131

@@ -142,6 +147,7 @@ class MockerCore:
142147
raw: HTTPResponse = ...,
143148
exc: Union[Exception, Type[Exception]] = ...,
144149
additional_matcher: Callable[[_RequestObjectProxy], bool] = ...,
150+
json_encoder: Optional[Type[JSONEncoder]] = ...,
145151
**kwargs: Any,
146152
) -> _Matcher: ...
147153

@@ -163,6 +169,7 @@ class MockerCore:
163169
raw: HTTPResponse = ...,
164170
exc: Union[Exception, Type[Exception]] = ...,
165171
additional_matcher: Callable[[_RequestObjectProxy], bool] = ...,
172+
json_encoder: Optional[Type[JSONEncoder]] = ...,
166173
**kwargs: Any,
167174
) -> _Matcher: ...
168175

@@ -184,6 +191,7 @@ class MockerCore:
184191
raw: HTTPResponse = ...,
185192
exc: Union[Exception, Type[Exception]] = ...,
186193
additional_matcher: Callable[[_RequestObjectProxy], bool] = ...,
194+
json_encoder: Optional[Type[JSONEncoder]] = ...,
187195
**kwargs: Any,
188196
) -> _Matcher: ...
189197

@@ -205,6 +213,7 @@ class MockerCore:
205213
raw: HTTPResponse = ...,
206214
exc: Union[Exception, Type[Exception]] = ...,
207215
additional_matcher: Callable[[_RequestObjectProxy], bool] = ...,
216+
json_encoder: Optional[Type[JSONEncoder]] = ...,
208217
**kwargs: Any,
209218
) -> _Matcher: ...
210219

@@ -226,6 +235,7 @@ class MockerCore:
226235
raw: HTTPResponse = ...,
227236
exc: Union[Exception, Type[Exception]] = ...,
228237
additional_matcher: Callable[[_RequestObjectProxy], bool] = ...,
238+
json_encoder: Optional[Type[JSONEncoder]] = ...,
229239
**kwargs: Any,
230240
) -> _Matcher: ...
231241

@@ -241,8 +251,9 @@ class Mocker(MockerCore):
241251
case_sensitive: bool = ...,
242252
adapter: Any = ...,
243253
session: Optional[Session] = ...,
244-
real_http: bool = ...) -> None:
245-
...
254+
real_http: bool = ...,
255+
json_encoder: Optional[Type[JSONEncoder]] = ...,
256+
) -> None: ...
246257
def __enter__(self) -> Any: ...
247258
def __exit__(self, type: Any, value: Any, traceback: Any) -> None: ...
248259
def __call__(self, obj: Any) -> Any: ...

requests_mock/response.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,13 @@
2424
from requests_mock import exceptions
2525

2626
_BODY_ARGS = frozenset(['raw', 'body', 'content', 'text', 'json'])
27-
_HTTP_ARGS = frozenset(['status_code', 'reason', 'headers', 'cookies'])
27+
_HTTP_ARGS = frozenset([
28+
'status_code',
29+
'reason',
30+
'headers',
31+
'cookies',
32+
'json_encoder',
33+
])
2834

2935
_DEFAULT_STATUS = 200
3036
_http_adapter = HTTPAdapter()
@@ -145,6 +151,7 @@ def create_response(request, **kwargs):
145151
:param unicode text: A text string to return upon a successful match.
146152
:param object json: A python object to be converted to a JSON string
147153
and returned upon a successful match.
154+
:param class json_encoder: Encoder object to use for JOSON.
148155
:param dict headers: A dictionary object containing headers that are
149156
returned upon a successful match.
150157
:param CookieJar cookies: A cookie jar with cookies to set on the
@@ -171,7 +178,8 @@ def create_response(request, **kwargs):
171178
raise TypeError('Text should be string data')
172179

173180
if json is not None:
174-
text = jsonutils.dumps(json)
181+
encoder = kwargs.pop('json_encoder', None) or jsonutils.JSONEncoder
182+
text = jsonutils.dumps(json, cls=encoder)
175183
if text is not None:
176184
encoding = get_encoding_from_headers(headers) or 'utf-8'
177185
content = text.encode(encoding)
@@ -265,6 +273,7 @@ def _call(f, *args, **kwargs):
265273
content=_call(self._params.get('content')),
266274
body=_call(self._params.get('body')),
267275
raw=self._params.get('raw'),
276+
json_encoder=self._params.get('json_encoder'),
268277
status_code=context.status_code,
269278
reason=context.reason,
270279
headers=context.headers,

tests/test_mocker.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
# License for the specific language governing permissions and limitations
1111
# under the License.
1212

13+
import json
1314
import pickle
1415

1516
import mock
@@ -600,3 +601,27 @@ def test_stream_zero_bytes(self, m):
600601

601602
full_val = res.raw.read()
602603
self.assertEqual(content, full_val)
604+
605+
def test_with_json_encoder_on_mocker(self):
606+
test_val = 'hello world'
607+
608+
class MyJsonEncoder(json.JSONEncoder):
609+
def encode(s, o):
610+
return test_val
611+
612+
with requests_mock.Mocker(json_encoder=MyJsonEncoder) as m:
613+
m.get("http://test", json={"a": "b"})
614+
res = requests.get("http://test")
615+
self.assertEqual(test_val, res.text)
616+
617+
@requests_mock.mock()
618+
def test_with_json_encoder_on_endpoint(self, m):
619+
test_val = 'hello world'
620+
621+
class MyJsonEncoder(json.JSONEncoder):
622+
def encode(s, o):
623+
return test_val
624+
625+
m.get("http://test", json={"a": "b"}, json_encoder=MyJsonEncoder)
626+
res = requests.get("http://test")
627+
self.assertEqual(test_val, res.text)

0 commit comments

Comments
 (0)