Codebase list responses / fresh-snapshots/main
New upstream snapshot. Debian Janitor 2 years ago
16 changed file(s) with 3817 addition(s) and 1322 deletion(s). Raw diff Collapse all Expand all
0 0.16.0
1 ------
2
3 * Fixed regression with `stream` parameter deprecation, requests.session() and cookie handling.
4 * Replaced adhoc URL parsing with `urllib.parse`.
5 * Added ``match`` parameter to ``add_callback`` method
6 * Added `responses.matchers.fragment_identifier_matcher`. This matcher allows you
7 to match request URL fragment identifier.
8 * Improved test coverage.
9 * Fixed failing test in python 2.7 when `python-future` is also installed.
10
11 0.15.0
12 ------
13
14 * Added `responses.PassthroughResponse` and
15 `reponses.BaseResponse.passthrough`. These features make building passthrough
16 responses more compatible with dynamcially generated response objects.
17 * Removed the unused ``_is_redirect()`` function from responses internals.
18 * Added `responses.matchers.request_kwargs_matcher`. This matcher allows you
19 to match additional request arguments like `stream`.
20 * Added `responses.matchers.multipart_matcher`. This matcher allows you
21 to match request body and headers for ``multipart/form-data`` data
22 * Added `responses.matchers.query_string_matcher`. This matcher allows you
23 to match request query string, similar to `responses.matchers.query_param_matcher`.
24 * Added `responses.matchers.header_matcher()`. This matcher allows you to match
25 request headers. By default only headers supplied to `header_matcher()` are checked.
26 You can make header matching exhaustive by passing `strict_match=True` to `header_matcher()`.
27 * Changed all matchers output message in case of mismatch. Now message is aligned
28 between Python2 and Python3 versions
29 * Deprecate ``stream`` argument in ``Response`` and ``CallbackResponse``
30 * Added Python 3.10 support
31
32 0.14.0
33 ------
34
35 * Added `responses.matchers`.
36 * Moved `responses.json_params_matcher` to `responses.matchers.json_params_matcher`
37 * Moved `responses.urlencoded_params_matcher` to
38 `responses.matchers.urlencoded_params_matcher`
39 * Added `responses.matchers.query_param_matcher`. This matcher allows you
40 to match query strings with a dictionary.
41 * Added `auto_calculate_content_length` option to `responses.add()`. When
42 enabled, this option will generate a `Content-Length` header
43 based on the number of bytes in the response body.
44
045 0.13.4
146 ------
247
00 include README.rst CHANGES LICENSE
1 include test_responses.py tox.ini
1 include test_responses.py test_matchers.py
2 include **/*.pyi
3 include tox.ini
24 global-exclude *~
+806
-549
PKG-INFO less more
00 Metadata-Version: 2.1
11 Name: responses
2 Version: 0.13.4
2 Version: 0.14.0
33 Summary: A utility library for mocking out the `requests` Python library.
44 Home-page: https://github.com/getsentry/responses
55 Author: David Cramer
66 License: Apache 2.0
7 Description: Responses
8 =========
9
10 .. image:: https://img.shields.io/pypi/v/responses.svg
11 :target: https://pypi.python.org/pypi/responses/
12
13 .. image:: https://travis-ci.org/getsentry/responses.svg?branch=master
14 :target: https://travis-ci.org/getsentry/responses
15
16 .. image:: https://img.shields.io/pypi/pyversions/responses.svg
17 :target: https://pypi.org/project/responses/
18
19 A utility library for mocking out the ``requests`` Python library.
20
21 .. note::
22
23 Responses requires Python 2.7 or newer, and requests >= 2.0
24
25
26 Installing
27 ----------
28
29 ``pip install responses``
30
31
32 Basics
33 ------
34
35 The core of ``responses`` comes from registering mock responses:
36
37 .. code-block:: python
38
39 import responses
40 import requests
41
42 @responses.activate
43 def test_simple():
44 responses.add(responses.GET, 'http://twitter.com/api/1/foobar',
45 json={'error': 'not found'}, status=404)
46
47 resp = requests.get('http://twitter.com/api/1/foobar')
48
49 assert resp.json() == {"error": "not found"}
50
51 assert len(responses.calls) == 1
52 assert responses.calls[0].request.url == 'http://twitter.com/api/1/foobar'
53 assert responses.calls[0].response.text == '{"error": "not found"}'
54
55 If you attempt to fetch a url which doesn't hit a match, ``responses`` will raise
56 a ``ConnectionError``:
57
58 .. code-block:: python
59
60 import responses
61 import requests
62
63 from requests.exceptions import ConnectionError
64
65 @responses.activate
66 def test_simple():
67 with pytest.raises(ConnectionError):
68 requests.get('http://twitter.com/api/1/foobar')
69
70 Lastly, you can pass an ``Exception`` as the body to trigger an error on the request:
71
72 .. code-block:: python
73
74 import responses
75 import requests
76
77 @responses.activate
78 def test_simple():
79 responses.add(responses.GET, 'http://twitter.com/api/1/foobar',
80 body=Exception('...'))
81 with pytest.raises(Exception):
82 requests.get('http://twitter.com/api/1/foobar')
83
84
85 Response Parameters
86 -------------------
87
88 Responses are automatically registered via params on ``add``, but can also be
89 passed directly:
90
91 .. code-block:: python
92
93 import responses
94
95 responses.add(
96 responses.Response(
97 method='GET',
98 url='http://example.com',
99 )
100 )
101
102 The following attributes can be passed to a Response mock:
103
104 method (``str``)
105 The HTTP method (GET, POST, etc).
106
107 url (``str`` or compiled regular expression)
108 The full resource URL.
109
110 match_querystring (``bool``)
111 Include the query string when matching requests.
112 Enabled by default if the response URL contains a query string,
113 disabled if it doesn't or the URL is a regular expression.
114
115 body (``str`` or ``BufferedReader``)
116 The response body.
117
118 json
119 A Python object representing the JSON response body. Automatically configures
120 the appropriate Content-Type.
121
122 status (``int``)
123 The HTTP status code.
124
125 content_type (``content_type``)
126 Defaults to ``text/plain``.
127
128 headers (``dict``)
129 Response headers.
130
131 stream (``bool``)
132 Disabled by default. Indicates the response should use the streaming API.
133
134 match (``list``)
135 A list of callbacks to match requests based on request body contents.
136
137
138 Matching Request Parameters
139 ---------------------------
140
141 When adding responses for endpoints that are sent request data you can add
142 matchers to ensure your code is sending the right parameters and provide
143 different responses based on the request body contents. Responses provides
144 matchers for JSON and URLencoded request bodies and you can supply your own for
145 other formats.
146
147 .. code-block:: python
148
149 import responses
150 import requests
151
152 @responses.activate
153 def test_calc_api():
154 responses.add(
155 responses.POST,
156 url='http://calc.com/sum',
157 body="4",
158 match=[
159 responses.urlencoded_params_matcher({"left": "1", "right": "3"})
160 ]
161 )
162 requests.post("http://calc.com/sum", data={"left": 1, "right": 3})
163
164 Matching JSON encoded data can be done with ``responses.json_params_matcher()``.
165 If your application uses other encodings you can build your own matcher that
166 returns ``True`` or ``False`` if the request parameters match. Your matcher can
167 expect a ``request_body`` parameter to be provided by responses.
168
169 Dynamic Responses
170 -----------------
171
172 You can utilize callbacks to provide dynamic responses. The callback must return
173 a tuple of (``status``, ``headers``, ``body``).
174
175 .. code-block:: python
176
177 import json
178
179 import responses
180 import requests
181
182 @responses.activate
183 def test_calc_api():
184
185 def request_callback(request):
186 payload = json.loads(request.body)
187 resp_body = {'value': sum(payload['numbers'])}
188 headers = {'request-id': '728d329e-0e86-11e4-a748-0c84dc037c13'}
189 return (200, headers, json.dumps(resp_body))
190
191 responses.add_callback(
192 responses.POST, 'http://calc.com/sum',
193 callback=request_callback,
194 content_type='application/json',
195 )
196
197 resp = requests.post(
198 'http://calc.com/sum',
199 json.dumps({'numbers': [1, 2, 3]}),
200 headers={'content-type': 'application/json'},
201 )
202
203 assert resp.json() == {'value': 6}
204
205 assert len(responses.calls) == 1
206 assert responses.calls[0].request.url == 'http://calc.com/sum'
207 assert responses.calls[0].response.text == '{"value": 6}'
208 assert (
209 responses.calls[0].response.headers['request-id'] ==
210 '728d329e-0e86-11e4-a748-0c84dc037c13'
211 )
212
213 You can also pass a compiled regex to ``add_callback`` to match multiple urls:
214
215 .. code-block:: python
216
217 import re, json
218
219 from functools import reduce
220
221 import responses
222 import requests
223
224 operators = {
225 'sum': lambda x, y: x+y,
226 'prod': lambda x, y: x*y,
227 'pow': lambda x, y: x**y
228 }
229
230 @responses.activate
231 def test_regex_url():
232
233 def request_callback(request):
234 payload = json.loads(request.body)
235 operator_name = request.path_url[1:]
236
237 operator = operators[operator_name]
238
239 resp_body = {'value': reduce(operator, payload['numbers'])}
240 headers = {'request-id': '728d329e-0e86-11e4-a748-0c84dc037c13'}
241 return (200, headers, json.dumps(resp_body))
242
243 responses.add_callback(
244 responses.POST,
245 re.compile('http://calc.com/(sum|prod|pow|unsupported)'),
246 callback=request_callback,
247 content_type='application/json',
248 )
249
250 resp = requests.post(
251 'http://calc.com/prod',
252 json.dumps({'numbers': [2, 3, 4]}),
253 headers={'content-type': 'application/json'},
254 )
255 assert resp.json() == {'value': 24}
256
257 test_regex_url()
258
259
260 If you want to pass extra keyword arguments to the callback function, for example when reusing
261 a callback function to give a slightly different result, you can use ``functools.partial``:
262
263 .. code-block:: python
264
265 from functools import partial
266
267 ...
268
269 def request_callback(request, id=None):
270 payload = json.loads(request.body)
271 resp_body = {'value': sum(payload['numbers'])}
272 headers = {'request-id': id}
273 return (200, headers, json.dumps(resp_body))
274
275 responses.add_callback(
276 responses.POST, 'http://calc.com/sum',
277 callback=partial(request_callback, id='728d329e-0e86-11e4-a748-0c84dc037c13'),
278 content_type='application/json',
279 )
280
281
282 You can see params passed in the original ``request`` in ``responses.calls[].request.params``:
283
284 .. code-block:: python
285
286 import responses
287 import requests
288
289 @responses.activate
290 def test_request_params():
291 responses.add(
292 method=responses.GET,
293 url="http://example.com?hello=world",
294 body="test",
295 match_querystring=False,
296 )
297
298 resp = requests.get('http://example.com', params={"hello": "world"})
299 assert responses.calls[0].request.params == {"hello": "world"}
300
301 Responses as a context manager
302 ------------------------------
303
304 .. code-block:: python
305
306 import responses
307 import requests
308
309 def test_my_api():
310 with responses.RequestsMock() as rsps:
311 rsps.add(responses.GET, 'http://twitter.com/api/1/foobar',
312 body='{}', status=200,
313 content_type='application/json')
314 resp = requests.get('http://twitter.com/api/1/foobar')
315
316 assert resp.status_code == 200
317
318 # outside the context manager requests will hit the remote server
319 resp = requests.get('http://twitter.com/api/1/foobar')
320 resp.status_code == 404
321
322 Responses as a pytest fixture
323 -----------------------------
324
325 .. code-block:: python
326
327 @pytest.fixture
328 def mocked_responses():
329 with responses.RequestsMock() as rsps:
330 yield rsps
331
332 def test_api(mocked_responses):
333 mocked_responses.add(
334 responses.GET, 'http://twitter.com/api/1/foobar',
335 body='{}', status=200,
336 content_type='application/json')
337 resp = requests.get('http://twitter.com/api/1/foobar')
338 assert resp.status_code == 200
339
340 Responses inside a unittest setUp()
341 -----------------------------------
342
343 When run with unittest tests, this can be used to set up some
344 generic class-level responses, that may be complemented by each test
345
346 .. code-block:: python
347
348 def setUp():
349 self.responses = responses.RequestsMock()
350 self.responses.start()
351
352 # self.responses.add(...)
353
354 self.addCleanup(self.responses.stop)
355 self.addCleanup(self.responses.reset)
356
357 def test_api(self):
358 self.responses.add(
359 responses.GET, 'http://twitter.com/api/1/foobar',
360 body='{}', status=200,
361 content_type='application/json')
362 resp = requests.get('http://twitter.com/api/1/foobar')
363 assert resp.status_code == 200
364
365 Assertions on declared responses
366 --------------------------------
367
368 When used as a context manager, Responses will, by default, raise an assertion
369 error if a url was registered but not accessed. This can be disabled by passing
370 the ``assert_all_requests_are_fired`` value:
371
372 .. code-block:: python
373
374 import responses
375 import requests
376
377 def test_my_api():
378 with responses.RequestsMock(assert_all_requests_are_fired=False) as rsps:
379 rsps.add(responses.GET, 'http://twitter.com/api/1/foobar',
380 body='{}', status=200,
381 content_type='application/json')
382
383 assert_call_count
384 -----------------
385
386 Assert that the request was called exactly n times.
387
388 .. code-block:: python
389
390 import responses
391 import requests
392
393 @responses.activate
394 def test_assert_call_count():
395 responses.add(responses.GET, "http://example.com")
396
397 requests.get("http://example.com")
398 assert responses.assert_call_count("http://example.com", 1) is True
399
400 requests.get("http://example.com")
401 with pytest.raises(AssertionError) as excinfo:
402 responses.assert_call_count("http://example.com", 1)
403 assert "Expected URL 'http://example.com' to be called 1 times. Called 2 times." in str(excinfo.value)
404
405
406 Multiple Responses
407 ------------------
408
409 You can also add multiple responses for the same url:
410
411 .. code-block:: python
412
413 import responses
414 import requests
415
416 @responses.activate
417 def test_my_api():
418 responses.add(responses.GET, 'http://twitter.com/api/1/foobar', status=500)
419 responses.add(responses.GET, 'http://twitter.com/api/1/foobar',
420 body='{}', status=200,
421 content_type='application/json')
422
423 resp = requests.get('http://twitter.com/api/1/foobar')
424 assert resp.status_code == 500
425 resp = requests.get('http://twitter.com/api/1/foobar')
426 assert resp.status_code == 200
427
428
429 Using a callback to modify the response
430 ---------------------------------------
431
432 If you use customized processing in `requests` via subclassing/mixins, or if you
433 have library tools that interact with `requests` at a low level, you may need
434 to add extended processing to the mocked Response object to fully simulate the
435 environment for your tests. A `response_callback` can be used, which will be
436 wrapped by the library before being returned to the caller. The callback
437 accepts a `response` as it's single argument, and is expected to return a
438 single `response` object.
439
440 .. code-block:: python
441
442 import responses
443 import requests
444
445 def response_callback(resp):
446 resp.callback_processed = True
447 return resp
448
449 with responses.RequestsMock(response_callback=response_callback) as m:
450 m.add(responses.GET, 'http://example.com', body=b'test')
451 resp = requests.get('http://example.com')
452 assert resp.text == "test"
453 assert hasattr(resp, 'callback_processed')
454 assert resp.callback_processed is True
455
456
457 Passing through real requests
458 -----------------------------
459
460 In some cases you may wish to allow for certain requests to pass through responses
461 and hit a real server. This can be done with the ``add_passthru`` methods:
462
463 .. code-block:: python
464
465 import responses
466
467 @responses.activate
468 def test_my_api():
469 responses.add_passthru('https://percy.io')
470
471 This will allow any requests matching that prefix, that is otherwise not registered
472 as a mock response, to passthru using the standard behavior.
473
474 Regex can be used like:
475
476 .. code-block:: python
477
478 responses.add_passthru(re.compile('https://percy.io/\\w+'))
479
480
481 Viewing/Modifying registered responses
482 --------------------------------------
483
484 Registered responses are available as a public method of the RequestMock
485 instance. It is sometimes useful for debugging purposes to view the stack of
486 registered responses which can be accessed via ``responses.registered()``.
487
488 The ``replace`` function allows a previously registered ``response`` to be
489 changed. The method signature is identical to ``add``. ``response`` s are
490 identified using ``method`` and ``url``. Only the first matched ``response`` is
491 replaced.
492
493 .. code-block:: python
494
495 import responses
496 import requests
497
498 @responses.activate
499 def test_replace():
500
501 responses.add(responses.GET, 'http://example.org', json={'data': 1})
502 responses.replace(responses.GET, 'http://example.org', json={'data': 2})
503
504 resp = requests.get('http://example.org')
505
506 assert resp.json() == {'data': 2}
507
508
509 The ``upsert`` function allows a previously registered ``response`` to be
510 changed like ``replace``. If the response is registered, the ``upsert`` function
511 will registered it like ``add``.
512
513 ``remove`` takes a ``method`` and ``url`` argument and will remove **all**
514 matched responses from the registered list.
515
516 Finally, ``reset`` will reset all registered responses.
517
518 Contributing
519 ------------
520
521 Responses uses several linting and autoformatting utilities, so it's important that when
522 submitting patches you use the appropriate toolchain:
523
524 Clone the repository:
525
526 .. code-block:: shell
527
528 git clone https://github.com/getsentry/responses.git
529
530 Create an environment (e.g. with ``virtualenv``):
531
532 .. code-block:: shell
533
534 virtualenv .env && source .env/bin/activate
535
536 Configure development requirements:
537
538 .. code-block:: shell
539
540 make develop
541
542 Responses uses `Pytest <https://docs.pytest.org/en/latest/>`_ for
543 testing. You can run all tests by:
544
545 .. code-block:: shell
546
547 pytest
548
549 And run a single test by:
550
551 .. code-block:: shell
552
553 pytest -k '<test_function_name>'
554
5557 Platform: UNKNOWN
5568 Classifier: Intended Audience :: Developers
5579 Classifier: Intended Audience :: System Administrators
56517 Classifier: Programming Language :: Python :: 3.7
56618 Classifier: Programming Language :: Python :: 3.8
56719 Classifier: Programming Language :: Python :: 3.9
20 Classifier: Programming Language :: Python :: 3.10
56821 Classifier: Topic :: Software Development
56922 Requires-Python: >=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*
57023 Description-Content-Type: text/x-rst
57124 Provides-Extra: tests
25 License-File: LICENSE
26
27 Responses
28 =========
29
30 .. image:: https://img.shields.io/pypi/v/responses.svg
31 :target: https://pypi.python.org/pypi/responses/
32
33 .. image:: https://img.shields.io/pypi/pyversions/responses.svg
34 :target: https://pypi.org/project/responses/
35
36 .. image:: https://codecov.io/gh/getsentry/responses/branch/master/graph/badge.svg
37 :target: https://codecov.io/gh/getsentry/responses/
38
39 A utility library for mocking out the ``requests`` Python library.
40
41 .. note::
42
43 Responses requires Python 2.7 or newer, and requests >= 2.0
44
45
46 Installing
47 ----------
48
49 ``pip install responses``
50
51
52 Basics
53 ------
54
55 The core of ``responses`` comes from registering mock responses:
56
57 .. code-block:: python
58
59 import responses
60 import requests
61
62 @responses.activate
63 def test_simple():
64 responses.add(responses.GET, 'http://twitter.com/api/1/foobar',
65 json={'error': 'not found'}, status=404)
66
67 resp = requests.get('http://twitter.com/api/1/foobar')
68
69 assert resp.json() == {"error": "not found"}
70
71 assert len(responses.calls) == 1
72 assert responses.calls[0].request.url == 'http://twitter.com/api/1/foobar'
73 assert responses.calls[0].response.text == '{"error": "not found"}'
74
75 If you attempt to fetch a url which doesn't hit a match, ``responses`` will raise
76 a ``ConnectionError``:
77
78 .. code-block:: python
79
80 import responses
81 import requests
82
83 from requests.exceptions import ConnectionError
84
85 @responses.activate
86 def test_simple():
87 with pytest.raises(ConnectionError):
88 requests.get('http://twitter.com/api/1/foobar')
89
90 Lastly, you can pass an ``Exception`` as the body to trigger an error on the request:
91
92 .. code-block:: python
93
94 import responses
95 import requests
96
97 @responses.activate
98 def test_simple():
99 responses.add(responses.GET, 'http://twitter.com/api/1/foobar',
100 body=Exception('...'))
101 with pytest.raises(Exception):
102 requests.get('http://twitter.com/api/1/foobar')
103
104
105 Response Parameters
106 -------------------
107
108 Responses are automatically registered via params on ``add``, but can also be
109 passed directly:
110
111 .. code-block:: python
112
113 import responses
114
115 responses.add(
116 responses.Response(
117 method='GET',
118 url='http://example.com',
119 )
120 )
121
122 The following attributes can be passed to a Response mock:
123
124 method (``str``)
125 The HTTP method (GET, POST, etc).
126
127 url (``str`` or compiled regular expression)
128 The full resource URL.
129
130 match_querystring (``bool``)
131 Include the query string when matching requests.
132 Enabled by default if the response URL contains a query string,
133 disabled if it doesn't or the URL is a regular expression.
134
135 body (``str`` or ``BufferedReader``)
136 The response body.
137
138 json
139 A Python object representing the JSON response body. Automatically configures
140 the appropriate Content-Type.
141
142 status (``int``)
143 The HTTP status code.
144
145 content_type (``content_type``)
146 Defaults to ``text/plain``.
147
148 headers (``dict``)
149 Response headers.
150
151 stream (``bool``)
152 DEPRECATED: use ``stream`` argument in request directly
153
154 auto_calculate_content_length (``bool``)
155 Disabled by default. Automatically calculates the length of a supplied string or JSON body.
156
157 match (``list``)
158 A list of callbacks to match requests based on request attributes.
159 Current module provides multiple matchers that you can use to match:
160
161 * body contents in JSON format
162 * body contents in URL encoded data format
163 * request query parameters
164 * request query string (similar to query parameters but takes string as input)
165 * kwargs provided to request e.g. ``stream``, ``verify``
166 * 'multipart/form-data' content and headers in request
167 * request headers
168 * request fragment identifier
169
170 Alternatively user can create custom matcher.
171 Read more `Matching Requests`_
172
173
174 Matching Requests
175 -----------------
176
177 When adding responses for endpoints that are sent request data you can add
178 matchers to ensure your code is sending the right parameters and provide
179 different responses based on the request body contents. Responses provides
180 matchers for JSON and URL-encoded request bodies and you can supply your own for
181 other formats.
182
183 .. code-block:: python
184
185 import responses
186 import requests
187 from responses import matchers
188
189 @responses.activate
190 def test_calc_api():
191 responses.add(
192 responses.POST,
193 url='http://calc.com/sum',
194 body="4",
195 match=[
196 matchers.urlencoded_params_matcher({"left": "1", "right": "3"})
197 ]
198 )
199 requests.post("http://calc.com/sum", data={"left": 1, "right": 3})
200
201 Matching JSON encoded data can be done with ``matchers.json_params_matcher()``.
202 If your application uses other encodings you can build your own matcher that
203 returns ``True`` or ``False`` if the request parameters match. Your matcher can
204 expect a ``request`` parameter to be provided by responses.
205
206 Similarly, you can use the ``matchers.query_param_matcher`` function to match
207 against the ``params`` request parameter.
208 Note, you must set ``match_querystring=False``
209
210 .. code-block:: python
211
212 import responses
213 import requests
214 from responses import matchers
215
216 @responses.activate
217 def test_calc_api():
218 url = "http://example.com/test"
219 params = {"hello": "world", "I am": "a big test"}
220 responses.add(
221 method=responses.GET,
222 url=url,
223 body="test",
224 match=[matchers.query_param_matcher(params)],
225 match_querystring=False,
226 )
227
228 resp = requests.get(url, params=params)
229
230 constructed_url = r"http://example.com/test?I+am=a+big+test&hello=world"
231 assert resp.url == constructed_url
232 assert resp.request.url == constructed_url
233 assert resp.request.params == params
234
235
236 As alternative, you can use query string value in ``matchers.query_string_matcher``
237
238 .. code-block:: python
239
240 import requests
241 import responses
242 from responses import matchers
243
244 @responses.activate
245 def my_func():
246 responses.add(
247 responses.GET,
248 "https://httpbin.org/get",
249 match=[matchers.query_string_matcher("didi=pro&test=1")],
250 )
251 resp = requests.get("https://httpbin.org/get", params={"test": 1, "didi": "pro"})
252
253 my_func()
254
255 To validate request arguments use the ``matchers.request_kwargs_matcher`` function to match
256 against the request kwargs.
257 Note, only arguments provided to ``matchers.request_kwargs_matcher`` will be validated
258
259 .. code-block:: python
260
261 import responses
262 import requests
263 from responses import matchers
264
265 with responses.RequestsMock(assert_all_requests_are_fired=False) as rsps:
266 req_kwargs = {
267 "stream": True,
268 "verify": False,
269 }
270 rsps.add(
271 "GET",
272 "http://111.com",
273 match=[matchers.request_kwargs_matcher(req_kwargs)],
274 )
275
276 requests.get("http://111.com", stream=True)
277
278 # >>> Arguments don't match: {stream: True, verify: True} doesn't match {stream: True, verify: False}
279
280 To validate request body and headers for ``multipart/form-data`` data you can use
281 ``matchers.multipart_matcher``. The ``data``, and ``files`` parameters provided will be compared
282 to the request:
283
284 .. code-block:: python
285
286 import requests
287 import responses
288 from responses.matchers import multipart_matcher
289
290 @responses.activate
291 def my_func():
292 req_data = {"some": "other", "data": "fields"}
293 req_files = {"file_name": b"Old World!"}
294 responses.add(
295 responses.POST, url="http://httpbin.org/post",
296 match=[multipart_matcher(req_data, req_files)]
297 )
298 resp = requests.post("http://httpbin.org/post", files={"file_name": b"New World!"})
299
300 my_func()
301 # >>> raises ConnectionError: multipart/form-data doesn't match. Request body differs.
302
303
304 To validate request URL fragment identifier you can use ``matchers.fragment_identifier_matcher``.
305 The matcher takes fragment string (everything after ``#`` sign) as input for comparison:
306
307 .. code-block:: python
308
309 import requests
310 import responses
311 from responses.matchers import fragment_identifier_matcher
312
313 @responses.activate
314 def run():
315 url = "http://example.com?ab=xy&zed=qwe#test=1&foo=bar"
316 responses.add(
317 responses.GET,
318 url,
319 match_querystring=True,
320 match=[fragment_identifier_matcher("test=1&foo=bar")],
321 body=b"test",
322 )
323
324 # two requests to check reversed order of fragment identifier
325 resp = requests.get("http://example.com?ab=xy&zed=qwe#test=1&foo=bar")
326 resp = requests.get("http://example.com?zed=qwe&ab=xy#foo=bar&test=1")
327
328 run()
329
330 Matching Request Headers
331 ------------------------
332
333 When adding responses you can specify matchers to ensure that your code is
334 sending the right headers and provide different responses based on the request
335 headers.
336
337 .. code-block:: python
338
339 import responses
340 import requests
341 from responses import matchers
342
343
344 @responses.activate
345 def test_content_type():
346 responses.add(
347 responses.GET,
348 url="http://example.com/",
349 body="hello world",
350 match=[
351 matchers.header_matcher({"Accept": "text/plain"})
352 ]
353 )
354
355 responses.add(
356 responses.GET,
357 url="http://example.com/",
358 json={"content": "hello world"},
359 match=[
360 matchers.header_matcher({"Accept": "application/json"})
361 ]
362 )
363
364 # request in reverse order to how they were added!
365 resp = requests.get("http://example.com/", headers={"Accept": "application/json"})
366 assert resp.json() == {"content": "hello world"}
367
368 resp = requests.get("http://example.com/", headers={"Accept": "text/plain"})
369 assert resp.text == "hello world"
370
371 Because ``requests`` will send several standard headers in addition to what was
372 specified by your code, request headers that are additional to the ones
373 passed to the matcher are ignored by default. You can change this behaviour by
374 passing ``strict_match=True`` to the matcher to ensure that only the headers
375 that you're expecting are sent and no others. Note that you will probably have
376 to use a ``PreparedRequest`` in your code to ensure that ``requests`` doesn't
377 include any additional headers.
378
379 .. code-block:: python
380
381 import responses
382 import requests
383 from responses import matchers
384
385 @responses.activate
386 def test_content_type():
387 responses.add(
388 responses.GET,
389 url="http://example.com/",
390 body="hello world",
391 match=[
392 matchers.header_matcher({"Accept": "text/plain"}, strict_match=True)
393 ]
394 )
395
396 # this will fail because requests adds its own headers
397 with pytest.raises(ConnectionError):
398 requests.get("http://example.com/", headers={"Accept": "text/plain"})
399
400 # a prepared request where you overwrite the headers before sending will work
401 session = requests.Session()
402 prepped = session.prepare_request(
403 requests.Request(
404 method="GET",
405 url="http://example.com/",
406 )
407 )
408 prepped.headers = {"Accept": "text/plain"}
409
410 resp = session.send(prepped)
411 assert resp.text == "hello world"
412
413 Dynamic Responses
414 -----------------
415
416 You can utilize callbacks to provide dynamic responses. The callback must return
417 a tuple of (``status``, ``headers``, ``body``).
418
419 .. code-block:: python
420
421 import json
422
423 import responses
424 import requests
425
426 @responses.activate
427 def test_calc_api():
428
429 def request_callback(request):
430 payload = json.loads(request.body)
431 resp_body = {'value': sum(payload['numbers'])}
432 headers = {'request-id': '728d329e-0e86-11e4-a748-0c84dc037c13'}
433 return (200, headers, json.dumps(resp_body))
434
435 responses.add_callback(
436 responses.POST, 'http://calc.com/sum',
437 callback=request_callback,
438 content_type='application/json',
439 )
440
441 resp = requests.post(
442 'http://calc.com/sum',
443 json.dumps({'numbers': [1, 2, 3]}),
444 headers={'content-type': 'application/json'},
445 )
446
447 assert resp.json() == {'value': 6}
448
449 assert len(responses.calls) == 1
450 assert responses.calls[0].request.url == 'http://calc.com/sum'
451 assert responses.calls[0].response.text == '{"value": 6}'
452 assert (
453 responses.calls[0].response.headers['request-id'] ==
454 '728d329e-0e86-11e4-a748-0c84dc037c13'
455 )
456
457 You can also pass a compiled regex to ``add_callback`` to match multiple urls:
458
459 .. code-block:: python
460
461 import re, json
462
463 from functools import reduce
464
465 import responses
466 import requests
467
468 operators = {
469 'sum': lambda x, y: x+y,
470 'prod': lambda x, y: x*y,
471 'pow': lambda x, y: x**y
472 }
473
474 @responses.activate
475 def test_regex_url():
476
477 def request_callback(request):
478 payload = json.loads(request.body)
479 operator_name = request.path_url[1:]
480
481 operator = operators[operator_name]
482
483 resp_body = {'value': reduce(operator, payload['numbers'])}
484 headers = {'request-id': '728d329e-0e86-11e4-a748-0c84dc037c13'}
485 return (200, headers, json.dumps(resp_body))
486
487 responses.add_callback(
488 responses.POST,
489 re.compile('http://calc.com/(sum|prod|pow|unsupported)'),
490 callback=request_callback,
491 content_type='application/json',
492 )
493
494 resp = requests.post(
495 'http://calc.com/prod',
496 json.dumps({'numbers': [2, 3, 4]}),
497 headers={'content-type': 'application/json'},
498 )
499 assert resp.json() == {'value': 24}
500
501 test_regex_url()
502
503
504 If you want to pass extra keyword arguments to the callback function, for example when reusing
505 a callback function to give a slightly different result, you can use ``functools.partial``:
506
507 .. code-block:: python
508
509 from functools import partial
510
511 ...
512
513 def request_callback(request, id=None):
514 payload = json.loads(request.body)
515 resp_body = {'value': sum(payload['numbers'])}
516 headers = {'request-id': id}
517 return (200, headers, json.dumps(resp_body))
518
519 responses.add_callback(
520 responses.POST, 'http://calc.com/sum',
521 callback=partial(request_callback, id='728d329e-0e86-11e4-a748-0c84dc037c13'),
522 content_type='application/json',
523 )
524
525
526 You can see params passed in the original ``request`` in ``responses.calls[].request.params``:
527
528 .. code-block:: python
529
530 import responses
531 import requests
532
533 @responses.activate
534 def test_request_params():
535 responses.add(
536 method=responses.GET,
537 url="http://example.com?hello=world",
538 body="test",
539 match_querystring=False,
540 )
541
542 resp = requests.get('http://example.com', params={"hello": "world"})
543 assert responses.calls[0].request.params == {"hello": "world"}
544
545 Responses as a context manager
546 ------------------------------
547
548 .. code-block:: python
549
550 import responses
551 import requests
552
553 def test_my_api():
554 with responses.RequestsMock() as rsps:
555 rsps.add(responses.GET, 'http://twitter.com/api/1/foobar',
556 body='{}', status=200,
557 content_type='application/json')
558 resp = requests.get('http://twitter.com/api/1/foobar')
559
560 assert resp.status_code == 200
561
562 # outside the context manager requests will hit the remote server
563 resp = requests.get('http://twitter.com/api/1/foobar')
564 resp.status_code == 404
565
566 Responses as a pytest fixture
567 -----------------------------
568
569 .. code-block:: python
570
571 @pytest.fixture
572 def mocked_responses():
573 with responses.RequestsMock() as rsps:
574 yield rsps
575
576 def test_api(mocked_responses):
577 mocked_responses.add(
578 responses.GET, 'http://twitter.com/api/1/foobar',
579 body='{}', status=200,
580 content_type='application/json')
581 resp = requests.get('http://twitter.com/api/1/foobar')
582 assert resp.status_code == 200
583
584 Responses inside a unittest setUp()
585 -----------------------------------
586
587 When run with unittest tests, this can be used to set up some
588 generic class-level responses, that may be complemented by each test
589
590 .. code-block:: python
591
592 def setUp():
593 self.responses = responses.RequestsMock()
594 self.responses.start()
595
596 # self.responses.add(...)
597
598 self.addCleanup(self.responses.stop)
599 self.addCleanup(self.responses.reset)
600
601 def test_api(self):
602 self.responses.add(
603 responses.GET, 'http://twitter.com/api/1/foobar',
604 body='{}', status=200,
605 content_type='application/json')
606 resp = requests.get('http://twitter.com/api/1/foobar')
607 assert resp.status_code == 200
608
609 Assertions on declared responses
610 --------------------------------
611
612 When used as a context manager, Responses will, by default, raise an assertion
613 error if a url was registered but not accessed. This can be disabled by passing
614 the ``assert_all_requests_are_fired`` value:
615
616 .. code-block:: python
617
618 import responses
619 import requests
620
621 def test_my_api():
622 with responses.RequestsMock(assert_all_requests_are_fired=False) as rsps:
623 rsps.add(responses.GET, 'http://twitter.com/api/1/foobar',
624 body='{}', status=200,
625 content_type='application/json')
626
627 assert_call_count
628 -----------------
629
630 Assert that the request was called exactly n times.
631
632 .. code-block:: python
633
634 import responses
635 import requests
636
637 @responses.activate
638 def test_assert_call_count():
639 responses.add(responses.GET, "http://example.com")
640
641 requests.get("http://example.com")
642 assert responses.assert_call_count("http://example.com", 1) is True
643
644 requests.get("http://example.com")
645 with pytest.raises(AssertionError) as excinfo:
646 responses.assert_call_count("http://example.com", 1)
647 assert "Expected URL 'http://example.com' to be called 1 times. Called 2 times." in str(excinfo.value)
648
649
650 Multiple Responses
651 ------------------
652
653 You can also add multiple responses for the same url:
654
655 .. code-block:: python
656
657 import responses
658 import requests
659
660 @responses.activate
661 def test_my_api():
662 responses.add(responses.GET, 'http://twitter.com/api/1/foobar', status=500)
663 responses.add(responses.GET, 'http://twitter.com/api/1/foobar',
664 body='{}', status=200,
665 content_type='application/json')
666
667 resp = requests.get('http://twitter.com/api/1/foobar')
668 assert resp.status_code == 500
669 resp = requests.get('http://twitter.com/api/1/foobar')
670 assert resp.status_code == 200
671
672
673 Using a callback to modify the response
674 ---------------------------------------
675
676 If you use customized processing in `requests` via subclassing/mixins, or if you
677 have library tools that interact with `requests` at a low level, you may need
678 to add extended processing to the mocked Response object to fully simulate the
679 environment for your tests. A `response_callback` can be used, which will be
680 wrapped by the library before being returned to the caller. The callback
681 accepts a `response` as it's single argument, and is expected to return a
682 single `response` object.
683
684 .. code-block:: python
685
686 import responses
687 import requests
688
689 def response_callback(resp):
690 resp.callback_processed = True
691 return resp
692
693 with responses.RequestsMock(response_callback=response_callback) as m:
694 m.add(responses.GET, 'http://example.com', body=b'test')
695 resp = requests.get('http://example.com')
696 assert resp.text == "test"
697 assert hasattr(resp, 'callback_processed')
698 assert resp.callback_processed is True
699
700
701 Passing through real requests
702 -----------------------------
703
704 In some cases you may wish to allow for certain requests to pass through responses
705 and hit a real server. This can be done with the ``add_passthru`` methods:
706
707 .. code-block:: python
708
709 import responses
710
711 @responses.activate
712 def test_my_api():
713 responses.add_passthru('https://percy.io')
714
715 This will allow any requests matching that prefix, that is otherwise not
716 registered as a mock response, to passthru using the standard behavior.
717
718 Pass through endpoints can be configured with regex patterns if you
719 need to allow an entire domain or path subtree to send requests:
720
721 .. code-block:: python
722
723 responses.add_passthru(re.compile('https://percy.io/\\w+'))
724
725
726 Lastly, you can use the `response.passthrough` attribute on `BaseResponse` or
727 use ``PassthroughResponse`` to enable a response to behave as a pass through.
728
729 .. code-block:: python
730
731 # Enable passthrough for a single response
732 response = Response(responses.GET, 'http://example.com', body='not used')
733 response.passthrough = True
734 responses.add(response)
735
736 # Use PassthroughResponse
737 response = PassthroughResponse(responses.GET, 'http://example.com')
738 responses.add(response)
739
740 Viewing/Modifying registered responses
741 --------------------------------------
742
743 Registered responses are available as a public method of the RequestMock
744 instance. It is sometimes useful for debugging purposes to view the stack of
745 registered responses which can be accessed via ``responses.registered()``.
746
747 The ``replace`` function allows a previously registered ``response`` to be
748 changed. The method signature is identical to ``add``. ``response`` s are
749 identified using ``method`` and ``url``. Only the first matched ``response`` is
750 replaced.
751
752 .. code-block:: python
753
754 import responses
755 import requests
756
757 @responses.activate
758 def test_replace():
759
760 responses.add(responses.GET, 'http://example.org', json={'data': 1})
761 responses.replace(responses.GET, 'http://example.org', json={'data': 2})
762
763 resp = requests.get('http://example.org')
764
765 assert resp.json() == {'data': 2}
766
767
768 The ``upsert`` function allows a previously registered ``response`` to be
769 changed like ``replace``. If the response is registered, the ``upsert`` function
770 will registered it like ``add``.
771
772 ``remove`` takes a ``method`` and ``url`` argument and will remove **all**
773 matched responses from the registered list.
774
775 Finally, ``reset`` will reset all registered responses.
776
777 Contributing
778 ------------
779
780 Responses uses several linting and autoformatting utilities, so it's important that when
781 submitting patches you use the appropriate toolchain:
782
783 Clone the repository:
784
785 .. code-block:: shell
786
787 git clone https://github.com/getsentry/responses.git
788
789 Create an environment (e.g. with ``virtualenv``):
790
791 .. code-block:: shell
792
793 virtualenv .env && source .env/bin/activate
794
795 Configure development requirements:
796
797 .. code-block:: shell
798
799 make develop
800
801 Responses uses `Pytest <https://docs.pytest.org/en/latest/>`_ for
802 testing. You can run all tests by:
803
804 .. code-block:: shell
805
806 pytest
807
808 And run a single test by:
809
810 .. code-block:: shell
811
812 pytest -k '<test_function_name>'
813
814 To verify ``type`` compliance, run `mypy <https://github.com/python/mypy>`_ linter:
815
816 .. code-block:: shell
817
818 mypy --config-file=./mypy.ini -p responses
819
820 To check code style and reformat it run:
821
822 .. code-block:: shell
823
824 pre-commit run --all-files
825
826 Note: on some OS, you have to use ``pre_commit``
827
828
33 .. image:: https://img.shields.io/pypi/v/responses.svg
44 :target: https://pypi.python.org/pypi/responses/
55
6 .. image:: https://travis-ci.org/getsentry/responses.svg?branch=master
7 :target: https://travis-ci.org/getsentry/responses
8
96 .. image:: https://img.shields.io/pypi/pyversions/responses.svg
107 :target: https://pypi.org/project/responses/
8
9 .. image:: https://codecov.io/gh/getsentry/responses/branch/master/graph/badge.svg
10 :target: https://codecov.io/gh/getsentry/responses/
1111
1212 A utility library for mocking out the ``requests`` Python library.
1313
122122 Response headers.
123123
124124 stream (``bool``)
125 Disabled by default. Indicates the response should use the streaming API.
125 DEPRECATED: use ``stream`` argument in request directly
126
127 auto_calculate_content_length (``bool``)
128 Disabled by default. Automatically calculates the length of a supplied string or JSON body.
126129
127130 match (``list``)
128 A list of callbacks to match requests based on request body contents.
129
130
131 Matching Request Parameters
132 ---------------------------
131 A list of callbacks to match requests based on request attributes.
132 Current module provides multiple matchers that you can use to match:
133
134 * body contents in JSON format
135 * body contents in URL encoded data format
136 * request query parameters
137 * request query string (similar to query parameters but takes string as input)
138 * kwargs provided to request e.g. ``stream``, ``verify``
139 * 'multipart/form-data' content and headers in request
140 * request headers
141 * request fragment identifier
142
143 Alternatively user can create custom matcher.
144 Read more `Matching Requests`_
145
146
147 Matching Requests
148 -----------------
133149
134150 When adding responses for endpoints that are sent request data you can add
135151 matchers to ensure your code is sending the right parameters and provide
136152 different responses based on the request body contents. Responses provides
137 matchers for JSON and URLencoded request bodies and you can supply your own for
153 matchers for JSON and URL-encoded request bodies and you can supply your own for
138154 other formats.
139155
140156 .. code-block:: python
141157
142158 import responses
143159 import requests
160 from responses import matchers
144161
145162 @responses.activate
146163 def test_calc_api():
149166 url='http://calc.com/sum',
150167 body="4",
151168 match=[
152 responses.urlencoded_params_matcher({"left": "1", "right": "3"})
169 matchers.urlencoded_params_matcher({"left": "1", "right": "3"})
153170 ]
154171 )
155172 requests.post("http://calc.com/sum", data={"left": 1, "right": 3})
156173
157 Matching JSON encoded data can be done with ``responses.json_params_matcher()``.
174 Matching JSON encoded data can be done with ``matchers.json_params_matcher()``.
158175 If your application uses other encodings you can build your own matcher that
159176 returns ``True`` or ``False`` if the request parameters match. Your matcher can
160 expect a ``request_body`` parameter to be provided by responses.
177 expect a ``request`` parameter to be provided by responses.
178
179 Similarly, you can use the ``matchers.query_param_matcher`` function to match
180 against the ``params`` request parameter.
181 Note, you must set ``match_querystring=False``
182
183 .. code-block:: python
184
185 import responses
186 import requests
187 from responses import matchers
188
189 @responses.activate
190 def test_calc_api():
191 url = "http://example.com/test"
192 params = {"hello": "world", "I am": "a big test"}
193 responses.add(
194 method=responses.GET,
195 url=url,
196 body="test",
197 match=[matchers.query_param_matcher(params)],
198 match_querystring=False,
199 )
200
201 resp = requests.get(url, params=params)
202
203 constructed_url = r"http://example.com/test?I+am=a+big+test&hello=world"
204 assert resp.url == constructed_url
205 assert resp.request.url == constructed_url
206 assert resp.request.params == params
207
208
209 As alternative, you can use query string value in ``matchers.query_string_matcher``
210
211 .. code-block:: python
212
213 import requests
214 import responses
215 from responses import matchers
216
217 @responses.activate
218 def my_func():
219 responses.add(
220 responses.GET,
221 "https://httpbin.org/get",
222 match=[matchers.query_string_matcher("didi=pro&test=1")],
223 )
224 resp = requests.get("https://httpbin.org/get", params={"test": 1, "didi": "pro"})
225
226 my_func()
227
228 To validate request arguments use the ``matchers.request_kwargs_matcher`` function to match
229 against the request kwargs.
230 Note, only arguments provided to ``matchers.request_kwargs_matcher`` will be validated
231
232 .. code-block:: python
233
234 import responses
235 import requests
236 from responses import matchers
237
238 with responses.RequestsMock(assert_all_requests_are_fired=False) as rsps:
239 req_kwargs = {
240 "stream": True,
241 "verify": False,
242 }
243 rsps.add(
244 "GET",
245 "http://111.com",
246 match=[matchers.request_kwargs_matcher(req_kwargs)],
247 )
248
249 requests.get("http://111.com", stream=True)
250
251 # >>> Arguments don't match: {stream: True, verify: True} doesn't match {stream: True, verify: False}
252
253 To validate request body and headers for ``multipart/form-data`` data you can use
254 ``matchers.multipart_matcher``. The ``data``, and ``files`` parameters provided will be compared
255 to the request:
256
257 .. code-block:: python
258
259 import requests
260 import responses
261 from responses.matchers import multipart_matcher
262
263 @responses.activate
264 def my_func():
265 req_data = {"some": "other", "data": "fields"}
266 req_files = {"file_name": b"Old World!"}
267 responses.add(
268 responses.POST, url="http://httpbin.org/post",
269 match=[multipart_matcher(req_data, req_files)]
270 )
271 resp = requests.post("http://httpbin.org/post", files={"file_name": b"New World!"})
272
273 my_func()
274 # >>> raises ConnectionError: multipart/form-data doesn't match. Request body differs.
275
276
277 To validate request URL fragment identifier you can use ``matchers.fragment_identifier_matcher``.
278 The matcher takes fragment string (everything after ``#`` sign) as input for comparison:
279
280 .. code-block:: python
281
282 import requests
283 import responses
284 from responses.matchers import fragment_identifier_matcher
285
286 @responses.activate
287 def run():
288 url = "http://example.com?ab=xy&zed=qwe#test=1&foo=bar"
289 responses.add(
290 responses.GET,
291 url,
292 match_querystring=True,
293 match=[fragment_identifier_matcher("test=1&foo=bar")],
294 body=b"test",
295 )
296
297 # two requests to check reversed order of fragment identifier
298 resp = requests.get("http://example.com?ab=xy&zed=qwe#test=1&foo=bar")
299 resp = requests.get("http://example.com?zed=qwe&ab=xy#foo=bar&test=1")
300
301 run()
302
303 Matching Request Headers
304 ------------------------
305
306 When adding responses you can specify matchers to ensure that your code is
307 sending the right headers and provide different responses based on the request
308 headers.
309
310 .. code-block:: python
311
312 import responses
313 import requests
314 from responses import matchers
315
316
317 @responses.activate
318 def test_content_type():
319 responses.add(
320 responses.GET,
321 url="http://example.com/",
322 body="hello world",
323 match=[
324 matchers.header_matcher({"Accept": "text/plain"})
325 ]
326 )
327
328 responses.add(
329 responses.GET,
330 url="http://example.com/",
331 json={"content": "hello world"},
332 match=[
333 matchers.header_matcher({"Accept": "application/json"})
334 ]
335 )
336
337 # request in reverse order to how they were added!
338 resp = requests.get("http://example.com/", headers={"Accept": "application/json"})
339 assert resp.json() == {"content": "hello world"}
340
341 resp = requests.get("http://example.com/", headers={"Accept": "text/plain"})
342 assert resp.text == "hello world"
343
344 Because ``requests`` will send several standard headers in addition to what was
345 specified by your code, request headers that are additional to the ones
346 passed to the matcher are ignored by default. You can change this behaviour by
347 passing ``strict_match=True`` to the matcher to ensure that only the headers
348 that you're expecting are sent and no others. Note that you will probably have
349 to use a ``PreparedRequest`` in your code to ensure that ``requests`` doesn't
350 include any additional headers.
351
352 .. code-block:: python
353
354 import responses
355 import requests
356 from responses import matchers
357
358 @responses.activate
359 def test_content_type():
360 responses.add(
361 responses.GET,
362 url="http://example.com/",
363 body="hello world",
364 match=[
365 matchers.header_matcher({"Accept": "text/plain"}, strict_match=True)
366 ]
367 )
368
369 # this will fail because requests adds its own headers
370 with pytest.raises(ConnectionError):
371 requests.get("http://example.com/", headers={"Accept": "text/plain"})
372
373 # a prepared request where you overwrite the headers before sending will work
374 session = requests.Session()
375 prepped = session.prepare_request(
376 requests.Request(
377 method="GET",
378 url="http://example.com/",
379 )
380 )
381 prepped.headers = {"Accept": "text/plain"}
382
383 resp = session.send(prepped)
384 assert resp.text == "hello world"
161385
162386 Dynamic Responses
163387 -----------------
461685 def test_my_api():
462686 responses.add_passthru('https://percy.io')
463687
464 This will allow any requests matching that prefix, that is otherwise not registered
465 as a mock response, to passthru using the standard behavior.
466
467 Regex can be used like:
688 This will allow any requests matching that prefix, that is otherwise not
689 registered as a mock response, to passthru using the standard behavior.
690
691 Pass through endpoints can be configured with regex patterns if you
692 need to allow an entire domain or path subtree to send requests:
468693
469694 .. code-block:: python
470695
471696 responses.add_passthru(re.compile('https://percy.io/\\w+'))
472697
698
699 Lastly, you can use the `response.passthrough` attribute on `BaseResponse` or
700 use ``PassthroughResponse`` to enable a response to behave as a pass through.
701
702 .. code-block:: python
703
704 # Enable passthrough for a single response
705 response = Response(responses.GET, 'http://example.com', body='not used')
706 response.passthrough = True
707 responses.add(response)
708
709 # Use PassthroughResponse
710 response = PassthroughResponse(responses.GET, 'http://example.com')
711 responses.add(response)
473712
474713 Viewing/Modifying registered responses
475714 --------------------------------------
544783 .. code-block:: shell
545784
546785 pytest -k '<test_function_name>'
786
787 To verify ``type`` compliance, run `mypy <https://github.com/python/mypy>`_ linter:
788
789 .. code-block:: shell
790
791 mypy --config-file=./mypy.ini -p responses
792
793 To check code style and reformat it run:
794
795 .. code-block:: shell
796
797 pre-commit run --all-files
798
799 Note: on some OS, you have to use ``pre_commit``
0 responses (0.13.4+git20211111.1.8a5a4b6-1) UNRELEASED; urgency=low
1
2 * New upstream snapshot.
3
4 -- Debian Janitor <janitor@jelmer.uk> Mon, 15 Nov 2021 21:32:27 -0000
5
06 responses (0.13.4-1) unstable; urgency=medium
17
28 * Team upload.
1212 from functools import update_wrapper
1313 from requests.adapters import HTTPAdapter
1414 from requests.exceptions import ConnectionError
15 from requests.sessions import REDIRECT_STATI
1615 from requests.utils import cookiejar_from_dict
16 from responses.matchers import json_params_matcher as _json_params_matcher
17 from responses.matchers import urlencoded_params_matcher as _urlencoded_params_matcher
18 from warnings import warn
1719
1820 try:
1921 from collections.abc import Sequence, Sized
2224
2325 try:
2426 from requests.packages.urllib3.response import HTTPResponse
25 except ImportError:
26 from urllib3.response import HTTPResponse
27 except ImportError: # pragma: no cover
28 from urllib3.response import HTTPResponse # pragma: no cover
2729 try:
2830 from requests.packages.urllib3.connection import HTTPHeaderDict
29 except ImportError:
30 from urllib3.response import HTTPHeaderDict
31 except ImportError: # pragma: no cover
32 from urllib3.response import HTTPHeaderDict # pragma: no cover
3133 try:
3234 from requests.packages.urllib3.util.url import parse_url
33 except ImportError:
34 from urllib3.util.url import parse_url
35 except ImportError: # pragma: no cover
36 from urllib3.util.url import parse_url # pragma: no cover
3537
3638 if six.PY2:
37 from urlparse import urlparse, parse_qsl, urlsplit, urlunsplit
39 from urlparse import urlparse, urlunparse, parse_qsl, urlsplit, urlunsplit
3840 from urllib import quote
3941 else:
40 from urllib.parse import urlparse, parse_qsl, urlsplit, urlunsplit, quote
42 from urllib.parse import (
43 urlparse,
44 urlunparse,
45 parse_qsl,
46 urlsplit,
47 urlunsplit,
48 quote,
49 )
4150
4251 if six.PY2:
4352 try:
5867 # Python 3.7
5968 Pattern = re.Pattern
6069
61 try:
62 from json.decoder import JSONDecodeError
63 except ImportError:
64 JSONDecodeError = ValueError
65
6670 UNSET = object()
6771
6872 Call = namedtuple("Call", ["request", "response"])
7074 _real_send = HTTPAdapter.send
7175
7276 logger = logging.getLogger("responses")
77
78
79 def urlencoded_params_matcher(params):
80 warn(
81 "Function is deprecated. Use 'from responses.matchers import urlencoded_params_matcher'",
82 DeprecationWarning,
83 )
84 return _urlencoded_params_matcher(params)
85
86
87 def json_params_matcher(params):
88 warn(
89 "Function is deprecated. Use 'from responses.matchers import json_params_matcher'",
90 DeprecationWarning,
91 )
92 return _json_params_matcher(params)
7393
7494
7595 def _is_string(s):
104124 return "".join(chars)
105125
106126
107 def _is_redirect(response):
108 try:
109 # 2.0.0 <= requests <= 2.2
110 return response.is_redirect
111
112 except AttributeError:
113 # requests > 2.2
114 return (
115 # use request.sessions conditional
116 response.status_code in REDIRECT_STATI
117 and "location" in response.headers
118 )
119
120
121127 def _ensure_str(s):
122128 if six.PY2:
123129 s = s.encode("utf-8") if isinstance(s, six.text_type) else s
126132
127133 def _cookies_from_headers(headers):
128134 try:
129 import http.cookies as cookies
130
131 resp_cookie = cookies.SimpleCookie()
135 import http.cookies as _cookies
136
137 resp_cookie = _cookies.SimpleCookie()
132138 resp_cookie.load(headers["set-cookie"])
133139
134140 cookies_dict = {name: v.value for name, v in resp_cookie.items()}
135 except ImportError:
141 except (ImportError, AttributeError):
136142 from cookies import Cookies
137143
138144 resp_cookies = Cookies.from_request(_ensure_str(headers["set-cookie"]))
156162
157163 # Preserve the argspec for the wrapped function so that testing
158164 # tools such as pytest can continue to use their fixture injection.
159 if hasattr(func, "__self__"):
160 args = args[1:] # Omit 'self'
161165 func_args = inspect.formatargspec(args, a, kw, None)
162166 else:
163167 signature = inspect.signature(func)
227231 return url
228232
229233
234 def _get_url_and_path(url):
235 url_parsed = urlparse(url)
236 url_and_path = urlunparse(
237 [url_parsed.scheme, url_parsed.netloc, url_parsed.path, None, None, None]
238 )
239 return parse_url(url_and_path).url
240
241
230242 def _handle_body(body):
231243 if isinstance(body, six.text_type):
232244 body = body.encode("utf-8")
239251 _unspecified = object()
240252
241253
242 def urlencoded_params_matcher(params):
243 def match(request_body):
244 return (
245 params is None
246 if request_body is None
247 else sorted(params.items()) == sorted(parse_qsl(request_body))
248 )
249
250 return match
251
252
253 def json_params_matcher(params):
254 def match(request_body):
255 try:
256 if isinstance(request_body, bytes):
257 request_body = request_body.decode("utf-8")
258 return (
259 params is None
260 if request_body is None
261 else params == json_module.loads(request_body)
262 )
263 except JSONDecodeError:
264 return False
265
266 return match
267
268
269254 class BaseResponse(object):
255 passthrough = False
270256 content_type = None
271257 headers = None
272258
273259 stream = False
274260
275 def __init__(self, method, url, match_querystring=_unspecified, match=[]):
261 def __init__(self, method, url, match_querystring=_unspecified, match=()):
276262 self.method = method
277263 # ensure the url has a default path set if the url is a string
278264 self.url = _ensure_url_default_path(url)
330316 if match_querystring:
331317 normalize_url = parse_url(url).url
332318 return self._url_matches_strict(normalize_url, other)
333
334319 else:
335 url_without_qs = url.split("?", 1)[0]
336 other_without_qs = other.split("?", 1)[0]
337 normalized_url_without_qs = parse_url(url_without_qs).url
338
339 return normalized_url_without_qs == other_without_qs
320 return _get_url_and_path(url) == _get_url_and_path(other)
340321
341322 elif isinstance(url, Pattern) and url.match(other):
342323 return True
344325 else:
345326 return False
346327
347 def _body_matches(self, match, request_body):
328 @staticmethod
329 def _req_attr_matches(match, request):
348330 for matcher in match:
349 if not matcher(request_body):
350 return False
351
352 return True
331 valid, reason = matcher(request)
332 if not valid:
333 return False, reason
334
335 return True, ""
353336
354337 def get_headers(self):
355338 headers = HTTPHeaderDict() # Duplicate headers are legal
369352 if not self._url_matches(self.url, request.url, self.match_querystring):
370353 return False, "URL does not match"
371354
372 if not self._body_matches(self.match, request.body):
373 return False, "Parameters do not match"
355 valid, reason = self._req_attr_matches(self.match, request)
356 if not valid:
357 return False, reason
374358
375359 return True, ""
376360
384368 json=None,
385369 status=200,
386370 headers=None,
387 stream=False,
371 stream=None,
388372 content_type=UNSET,
373 auto_calculate_content_length=False,
389374 **kwargs
390375 ):
391376 # if we were passed a `json` argument,
405390 self.body = body
406391 self.status = status
407392 self.headers = headers
393
394 if stream is not None:
395 warn(
396 "stream argument is deprecated. Use stream parameter in request directly",
397 DeprecationWarning,
398 )
399
408400 self.stream = stream
409401 self.content_type = content_type
402 self.auto_calculate_content_length = auto_calculate_content_length
410403 super(Response, self).__init__(method, url, **kwargs)
411404
412405 def get_response(self, request):
416409 headers = self.get_headers()
417410 status = self.status
418411 body = _handle_body(self.body)
412
413 if (
414 self.auto_calculate_content_length
415 and isinstance(body, BufferIO)
416 and "Content-Length" not in headers
417 ):
418 content_length = len(body.getvalue())
419 headers["Content-Length"] = str(content_length)
420
419421 return HTTPResponse(
420422 status=status,
421423 reason=six.moves.http_client.responses.get(status),
439441
440442 class CallbackResponse(BaseResponse):
441443 def __init__(
442 self, method, url, callback, stream=False, content_type="text/plain", **kwargs
444 self, method, url, callback, stream=None, content_type="text/plain", **kwargs
443445 ):
444446 self.callback = callback
447
448 if stream is not None:
449 warn(
450 "stream argument is deprecated. Use stream parameter in request directly",
451 DeprecationWarning,
452 )
445453 self.stream = stream
446454 self.content_type = content_type
447455 super(CallbackResponse, self).__init__(method, url, **kwargs)
483491 )
484492
485493
494 class PassthroughResponse(BaseResponse):
495 passthrough = True
496
497
486498 class OriginalResponseShim(object):
487499 """
488500 Shim for compatibility with older versions of urllib3
501513
502514 def isclosed(self):
503515 return True
516
517 def close(self):
518 return
504519
505520
506521 class RequestsMock(object):
654669 self.add(method_or_response, url, body, *args, **kwargs)
655670
656671 def add_callback(
657 self, method, url, callback, match_querystring=False, content_type="text/plain"
672 self,
673 method,
674 url,
675 callback,
676 match_querystring=False,
677 content_type="text/plain",
678 match=(),
658679 ):
659680 # ensure the url has a default path set if the url is a string
660681 # url = _ensure_url_default_path(url, match_querystring)
666687 callback=callback,
667688 content_type=content_type,
668689 match_querystring=match_querystring,
690 match=match,
669691 )
670692 )
671693
690712 return get_wrapped(func, self)
691713
692714 def _find_match(self, request):
715 """
716 Iterates through all available matches and validates if any of them matches the request
717
718 :param request: (PreparedRequest), request object
719 :return:
720 (Response) found match. If multiple found, then remove & return the first match.
721 (list) list with reasons why other matches don't match
722 """
693723 found = None
694724 found_match = None
695725 match_failed_reasons = []
716746 return params
717747
718748 def _on_request(self, adapter, request, **kwargs):
749 # add attributes params and req_kwargs to 'request' object for further match comparison
750 # original request object does not have these attributes
751 request.params = self._parse_request_params(request.path_url)
752 request.req_kwargs = kwargs
753
719754 match, match_failed_reasons = self._find_match(request)
720755 resp_callback = self.response_callback
721 request.params = self._parse_request_params(request.path_url)
722756
723757 if match is None:
724758 if any(
751785 response = resp_callback(response) if resp_callback else response
752786 raise response
753787
754 try:
755 response = adapter.build_response(request, match.get_response(request))
756 except BaseException as response:
757 match.call_count += 1
758 self._calls.add(request, response)
759 response = resp_callback(response) if resp_callback else response
760 raise
761
762 if not match.stream:
763 response.content # NOQA
788 if match.passthrough:
789 logger.info("request.passthrough-response", extra={"url": request.url})
790 response = _real_send(adapter, request, **kwargs)
791 else:
792 try:
793 response = adapter.build_response(request, match.get_response(request))
794 except BaseException as response:
795 match.call_count += 1
796 self._calls.add(request, response)
797 response = resp_callback(response) if resp_callback else response
798 raise
799
800 stream = kwargs.get("stream")
801 if not stream:
802 response.content # NOQA required to ensure that response body is read.
803 response.close()
764804
765805 response = resp_callback(response) if resp_callback else response
766806 match.call_count += 1
0 from collections import Sequence, Sized
1 from typing import (
2 Any,
3 Callable,
4 Iterator,
5 Mapping,
6 Optional,
7 NamedTuple,
8 Protocol,
9 TypeVar,
10 Dict,
11 List,
12 Tuple,
13 Union,
14 Iterable
15 )
16 from io import BufferedReader, BytesIO
17 from re import Pattern
18 from requests.adapters import HTTPResponse, PreparedRequest
19 from requests.cookies import RequestsCookieJar
20 from typing_extensions import Literal
21 from unittest import mock as std_mock
22 from urllib.parse import quote as quote
23 from urllib3.response import HTTPHeaderDict
24 from .matchers import urlencoded_params_matcher, json_params_matcher
25
26
27 def _clean_unicode(url: str) -> str: ...
28 def _cookies_from_headers(headers: Dict[str, str]) -> RequestsCookieJar: ...
29 def _ensure_str(s: str) -> str: ...
30 def _ensure_url_default_path(
31 url: Union[Pattern[str], str]
32 ) -> Union[Pattern[str], str]: ...
33 def _get_url_and_path(url: str) -> str: ...
34 def _handle_body(
35 body: Optional[Union[bytes, BufferedReader, str]]
36 ) -> Union[BufferedReader, BytesIO]: ...
37 def _has_unicode(s: str) -> bool: ...
38 def _is_string(s: Union[Pattern[str], str]) -> bool: ...
39 def get_wrapped(
40 func: Callable[..., Any], responses: RequestsMock
41 ) -> Callable[..., Any]: ...
42
43
44 class Call(NamedTuple):
45 request: PreparedRequest
46 response: Any
47
48 _Body = Union[str, BaseException, "Response", BufferedReader, bytes]
49
50 MatcherIterable = Iterable[Callable[[Any], Callable[..., Any]]]
51
52 class CallList(Sequence[Call], Sized):
53 def __init__(self) -> None: ...
54 def __iter__(self) -> Iterator[Call]: ...
55 def __len__(self) -> int: ...
56 def __getitem__(self, idx: int) -> Call: ... # type: ignore [override]
57 def add(self, request: PreparedRequest, response: _Body) -> None: ...
58 def reset(self) -> None: ...
59
60 class BaseResponse:
61 passthrough: bool = ...
62 content_type: Optional[str] = ...
63 headers: Optional[Mapping[str, str]] = ...
64 stream: bool = ...
65 method: Any = ...
66 url: Any = ...
67 match_querystring: Any = ...
68 match: MatcherIterable = ...
69 call_count: int = ...
70 def __init__(
71 self,
72 method: str,
73 url: Union[Pattern[str], str],
74 match_querystring: Union[bool, object] = ...,
75 match: MatcherIterable = ...,
76 ) -> None: ...
77 def __eq__(self, other: Any) -> bool: ...
78 def __ne__(self, other: Any) -> bool: ...
79 def _req_attr_matches(
80 self, match: MatcherIterable, request: Optional[Union[bytes, str]]
81 ) -> Tuple[bool, str]: ...
82 def _should_match_querystring(
83 self, match_querystring_argument: Union[bool, object]
84 ) -> bool: ...
85 def _url_matches(
86 self, url: Union[Pattern[str], str], other: str, match_querystring: bool = ...
87 ) -> bool: ...
88 def _url_matches_strict(self, url: str, other: str) -> bool: ...
89 def get_headers(self) -> HTTPHeaderDict: ... # type: ignore [no-any-unimported]
90 def get_response(self, request: PreparedRequest) -> None: ...
91 def matches(self, request: PreparedRequest) -> Tuple[bool, str]: ...
92
93 class Response(BaseResponse):
94 body: _Body = ...
95 status: int = ...
96 headers: Optional[Mapping[str, str]] = ...
97 stream: bool = ...
98 content_type: Optional[str] = ...
99 auto_calculate_content_length: bool = ...
100 def __init__(
101 self,
102 method: str,
103 url: Union[Pattern[str], str],
104 body: _Body = ...,
105 json: Optional[Any] = ...,
106 status: int = ...,
107 headers: Optional[Mapping[str, str]] = ...,
108 stream: bool = ...,
109 content_type: Optional[str] = ...,
110 auto_calculate_content_length: bool = ...,
111 match_querystring: bool = ...,
112 match: MatcherIterable = ...,
113 ) -> None: ...
114 def get_response( # type: ignore [override]
115 self, request: PreparedRequest
116 ) -> HTTPResponse: ...
117
118 class CallbackResponse(BaseResponse):
119 callback: Callable[[Any], Any] = ...
120 stream: bool = ...
121 content_type: Optional[str] = ...
122 def __init__(
123 self,
124 method: str,
125 url: Union[Pattern[str], str],
126 callback: Callable[[Any], Any],
127 stream: bool = ...,
128 content_type: Optional[str] = ...,
129 match_querystring: bool = ...,
130 match: MatcherIterable = ...,
131 ) -> None: ...
132 def get_response( # type: ignore [override]
133 self, request: PreparedRequest
134 ) -> HTTPResponse: ...
135
136 class PassthroughResponse(BaseResponse):
137 passthrough: bool = ...
138
139 class OriginalResponseShim:
140 msg: Any = ...
141 def __init__( # type: ignore [no-any-unimported]
142 self, headers: HTTPHeaderDict
143 ) -> None: ...
144 def isclosed(self) -> bool: ...
145
146 class RequestsMock:
147 DELETE: Literal["DELETE"]
148 GET: Literal["GET"]
149 HEAD: Literal["HEAD"]
150 OPTIONS: Literal["OPTIONS"]
151 PATCH: Literal["PATCH"]
152 POST: Literal["POST"]
153 PUT: Literal["PUT"]
154 response_callback: Optional[Callable[[Any], Any]] = ...
155 assert_all_requests_are_fired: Any = ...
156 passthru_prefixes: Tuple[str, ...] = ...
157 target: Any = ...
158 _matches: List[Any]
159 def __init__(
160 self,
161 assert_all_requests_are_fired: bool = ...,
162 response_callback: Optional[Callable[[Any], Any]] = ...,
163 passthru_prefixes: Tuple[str, ...] = ...,
164 target: str = ...,
165 ) -> None: ...
166 def reset(self) -> None: ...
167 add: _Add
168 add_passthru: _AddPassthru
169 def remove(
170 self,
171 method_or_response: Optional[Union[str, Response]] = ...,
172 url: Optional[Union[Pattern[str], str]] = ...,
173 ) -> None: ...
174 replace: _Replace
175 add_callback: _AddCallback
176 @property
177 def calls(self) -> CallList: ...
178 def __enter__(self) -> RequestsMock: ...
179 def __exit__(self, type: Any, value: Any, traceback: Any) -> bool: ...
180 activate: _Activate
181 def start(self) -> None: ...
182 def stop(self, allow_assert: bool = ...) -> None: ...
183 def assert_call_count(self, url: str, count: int) -> bool: ...
184 def registered(self) -> List[Any]: ...
185
186 _F = TypeVar("_F", bound=Callable[..., Any])
187
188 HeaderSet = Optional[Union[Mapping[str, str], List[Tuple[str, str]]]]
189
190 class _Activate(Protocol):
191 def __call__(self, func: _F) -> _F: ...
192
193 class _Add(Protocol):
194 def __call__(
195 self,
196 method: Optional[Union[str, BaseResponse]] = ...,
197 url: Optional[Union[Pattern[str], str]] = ...,
198 body: _Body = ...,
199 json: Optional[Any] = ...,
200 status: int = ...,
201 headers: HeaderSet = ...,
202 stream: bool = ...,
203 content_type: Optional[str] = ...,
204 auto_calculate_content_length: bool = ...,
205 adding_headers: HeaderSet = ...,
206 match_querystring: bool = ...,
207 match: MatcherIterable = ...,
208 ) -> None: ...
209
210 class _AddCallback(Protocol):
211 def __call__(
212 self,
213 method: str,
214 url: Union[Pattern[str], str],
215 callback: Callable[[PreparedRequest], Union[Exception, Tuple[int, Mapping[str, str], _Body]]],
216 match_querystring: bool = ...,
217 content_type: Optional[str] = ...,
218 match: MatcherIterable = ...,
219 ) -> None: ...
220
221 class _AddPassthru(Protocol):
222 def __call__(
223 self, prefix: Union[Pattern[str], str]
224 ) -> None: ...
225
226 class _Remove(Protocol):
227 def __call__(
228 self,
229 method_or_response: Optional[Union[str, BaseResponse]] = ...,
230 url: Optional[Union[Pattern[str], str]] = ...,
231 ) -> None: ...
232
233 class _Replace(Protocol):
234 def __call__(
235 self,
236 method_or_response: Optional[Union[str, BaseResponse]] = ...,
237 url: Optional[Union[Pattern[str], str]] = ...,
238 body: _Body = ...,
239 json: Optional[Any] = ...,
240 status: int = ...,
241 headers: HeaderSet = ...,
242 stream: bool = ...,
243 content_type: Optional[str] = ...,
244 adding_headers: HeaderSet = ...,
245 match_querystring: bool = ...,
246 match: MatcherIterable = ...,
247 ) -> None: ...
248
249 class _Upsert(Protocol):
250 def __call__(
251 self,
252 method: Optional[Union[str, BaseResponse]] = ...,
253 url: Optional[Union[Pattern[str], str]] = ...,
254 body: _Body = ...,
255 json: Optional[Any] = ...,
256 status: int = ...,
257 headers: HeaderSet = ...,
258 stream: bool = ...,
259 content_type: Optional[str] = ...,
260 adding_headers: HeaderSet = ...,
261 match_querystring: bool = ...,
262 match: MatcherIterable = ...,
263 ) -> None: ...
264
265 class _Registered(Protocol):
266 def __call__(self) -> List[Response]: ...
267
268
269 activate: _Activate
270 add: _Add
271 add_callback: _AddCallback
272 add_passthru: _AddPassthru
273 assert_all_requests_are_fired: bool
274 assert_call_count: Callable[[str, int], bool]
275 calls: CallList
276 DELETE: Literal["DELETE"]
277 GET: Literal["GET"]
278 HEAD: Literal["HEAD"]
279 mock: RequestsMock
280 _default_mock: RequestsMock
281 OPTIONS: Literal["OPTIONS"]
282 passthru_prefixes: Tuple[str, ...]
283 PATCH: Literal["PATCH"]
284 POST: Literal["POST"]
285 PUT: Literal["PUT"]
286 registered: _Registered
287 remove: _Remove
288 replace: _Replace
289 reset: Callable[[], None]
290 response_callback: Callable[[Any], Any]
291 start: Callable[[], None]
292 stop: Callable[..., None]
293 target: Any
294 upsert: _Upsert
295
296 __all__ = [
297 "CallbackResponse",
298 "Response",
299 "RequestsMock",
300 # Exposed by the RequestsMock class:
301 "activate",
302 "add",
303 "add_callback",
304 "add_passthru",
305 "assert_all_requests_are_fired",
306 "assert_call_count",
307 "calls",
308 "DELETE",
309 "GET",
310 "HEAD",
311 "OPTIONS",
312 "passthru_prefixes",
313 "PATCH",
314 "POST",
315 "PUT",
316 "registered",
317 "remove",
318 "replace",
319 "reset",
320 "response_callback",
321 "start",
322 "stop",
323 "target",
324 "upsert"
325 ]
0 import six
1 import json as json_module
2
3 from requests import PreparedRequest
4
5 if six.PY2:
6 from urlparse import parse_qsl, urlparse
7 else:
8 from urllib.parse import parse_qsl, urlparse
9
10
11 try:
12 from requests.packages.urllib3.util.url import parse_url
13 except ImportError: # pragma: no cover
14 from urllib3.util.url import parse_url # pragma: no cover
15
16 try:
17 from json.decoder import JSONDecodeError
18 except ImportError:
19 JSONDecodeError = ValueError
20
21
22 def _create_key_val_str(input_dict):
23 """
24 Returns string of format {'key': val, 'key2': val2}
25 Function is called recursively for nested dictionaries
26
27 :param input_dict: dictionary to transform
28 :return: (str) reformatted string
29 """
30
31 def list_to_str(input_list):
32 """
33 Convert all list items to string.
34 Function is called recursively for nested lists
35 """
36 converted_list = []
37 for item in sorted(input_list, key=lambda x: str(x)):
38 if isinstance(item, dict):
39 item = _create_key_val_str(item)
40 elif isinstance(item, list):
41 item = list_to_str(item)
42
43 converted_list.append(str(item))
44 list_str = ", ".join(converted_list)
45 return "[" + list_str + "]"
46
47 items_list = []
48 for key in sorted(input_dict.keys(), key=lambda x: str(x)):
49 val = input_dict[key]
50 if isinstance(val, dict):
51 val = _create_key_val_str(val)
52 elif isinstance(val, list):
53 val = list_to_str(input_list=val)
54
55 items_list.append("{}: {}".format(key, val))
56
57 key_val_str = "{{{}}}".format(", ".join(items_list))
58 return key_val_str
59
60
61 def urlencoded_params_matcher(params):
62 """
63 Matches URL encoded data
64
65 :param params: (dict) data provided to 'data' arg of request
66 :return: (func) matcher
67 """
68
69 def match(request):
70 reason = ""
71 request_body = request.body
72 qsl_body = dict(parse_qsl(request_body)) if request_body else {}
73 params_dict = params or {}
74 valid = params is None if request_body is None else params_dict == qsl_body
75 if not valid:
76 reason = "request.body doesn't match: {} doesn't match {}".format(
77 _create_key_val_str(qsl_body), _create_key_val_str(params_dict)
78 )
79
80 return valid, reason
81
82 return match
83
84
85 def json_params_matcher(params):
86 """
87 Matches JSON encoded data
88
89 :param params: (dict) JSON data provided to 'json' arg of request
90 :return: (func) matcher
91 """
92
93 def match(request):
94 reason = ""
95 request_body = request.body
96 params_dict = params or {}
97 try:
98 if isinstance(request_body, bytes):
99 request_body = request_body.decode("utf-8")
100 json_body = json_module.loads(request_body) if request_body else {}
101
102 valid = params is None if request_body is None else params_dict == json_body
103
104 if not valid:
105 reason = "request.body doesn't match: {} doesn't match {}".format(
106 _create_key_val_str(json_body), _create_key_val_str(params_dict)
107 )
108
109 except JSONDecodeError:
110 valid = False
111 reason = (
112 "request.body doesn't match: JSONDecodeError: Cannot parse request.body"
113 )
114
115 return valid, reason
116
117 return match
118
119
120 def fragment_identifier_matcher(identifier):
121 def match(request):
122 reason = ""
123 url_fragment = urlparse(request.url).fragment
124 if identifier:
125 url_fragment_qsl = sorted(parse_qsl(url_fragment))
126 identifier_qsl = sorted(parse_qsl(identifier))
127 valid = identifier_qsl == url_fragment_qsl
128 else:
129 valid = not url_fragment
130
131 if not valid:
132 reason = "URL fragment identifier is different: {} doesn't match {}".format(
133 identifier, url_fragment
134 )
135 return valid, reason
136
137 return match
138
139
140 def query_param_matcher(params):
141 """
142 Matcher to match 'params' argument in request
143
144 :param params: (dict), same as provided to request
145 :return: (func) matcher
146 """
147
148 def match(request):
149 reason = ""
150 request_params = request.params
151 request_params_dict = request_params or {}
152 params_dict = params or {}
153 valid = (
154 params is None
155 if request_params is None
156 else params_dict == request_params_dict
157 )
158
159 if not valid:
160 reason = "Parameters do not match. {} doesn't match {}".format(
161 _create_key_val_str(request_params_dict),
162 _create_key_val_str(params_dict),
163 )
164
165 return valid, reason
166
167 return match
168
169
170 def query_string_matcher(query):
171 """
172 Matcher to match query string part of request
173
174 :param query: (str), same as constructed by request
175 :return: (func) matcher
176 """
177
178 def match(request):
179 reason = ""
180 data = parse_url(request.url)
181 request_query = data.query
182
183 request_qsl = sorted(parse_qsl(request_query))
184 matcher_qsl = sorted(parse_qsl(query))
185
186 valid = not query if request_query is None else request_qsl == matcher_qsl
187
188 if not valid:
189 reason = "Query string doesn't match. {} doesn't match {}".format(
190 _create_key_val_str(dict(request_qsl)),
191 _create_key_val_str(dict(matcher_qsl)),
192 )
193
194 return valid, reason
195
196 return match
197
198
199 def request_kwargs_matcher(kwargs):
200 """
201 Matcher to match keyword arguments provided to request
202
203 :param kwargs: (dict), keyword arguments, same as provided to request
204 :return: (func) matcher
205 """
206
207 def match(request):
208 reason = ""
209 kwargs_dict = kwargs or {}
210 # validate only kwargs that were requested for comparison, skip defaults
211 request_kwargs = {
212 k: v for k, v in request.req_kwargs.items() if k in kwargs_dict
213 }
214
215 valid = (
216 not kwargs_dict
217 if not request_kwargs
218 else sorted(kwargs.items()) == sorted(request_kwargs.items())
219 )
220
221 if not valid:
222 reason = "Arguments don't match: {} doesn't match {}".format(
223 _create_key_val_str(request_kwargs), _create_key_val_str(kwargs_dict)
224 )
225
226 return valid, reason
227
228 return match
229
230
231 def multipart_matcher(files, data=None):
232 """
233 Matcher to match 'multipart/form-data' content-type.
234 This function constructs request body and headers from provided 'data' and 'files'
235 arguments and compares to actual request
236
237 :param files: (dict), same as provided to request
238 :param data: (dict), same as provided to request
239 :return: (func) matcher
240 """
241 if not files:
242 raise TypeError("files argument cannot be empty")
243
244 prepared = PreparedRequest()
245 prepared.headers = {"Content-Type": ""}
246 prepared.prepare_body(data=data, files=files)
247
248 def get_boundary(content_type):
249 """
250 Parse 'boundary' value from header.
251
252 :param content_type: (str) headers["Content-Type"] value
253 :return: (str) boundary value
254 """
255 if "boundary=" not in content_type:
256 return ""
257
258 return content_type.split("boundary=")[1]
259
260 def match(request):
261 reason = "multipart/form-data doesn't match. "
262 if "Content-Type" not in request.headers:
263 return False, reason + "Request is missing the 'Content-Type' header"
264
265 request_boundary = get_boundary(request.headers["Content-Type"])
266 prepared_boundary = get_boundary(prepared.headers["Content-Type"])
267
268 # replace boundary value in header and in body, since by default
269 # urllib3.filepost.encode_multipart_formdata dynamically calculates
270 # random boundary alphanumeric value
271 request_content_type = request.headers["Content-Type"]
272 prepared_content_type = prepared.headers["Content-Type"].replace(
273 prepared_boundary, request_boundary
274 )
275
276 request_body = request.body
277 if isinstance(request_body, bytes):
278 request_body = request_body.decode("utf-8")
279
280 prepared_body = prepared.body
281 if isinstance(prepared_body, bytes):
282 prepared_body = prepared_body.decode("utf-8")
283
284 prepared_body = prepared_body.replace(prepared_boundary, request_boundary)
285
286 headers_valid = prepared_content_type == request_content_type
287 if not headers_valid:
288 return (
289 False,
290 reason
291 + "Request headers['Content-Type'] is different. {} isn't equal to {}".format(
292 request_content_type, prepared_content_type
293 ),
294 )
295
296 body_valid = prepared_body == request_body
297 if not body_valid:
298 return False, reason + "Request body differs. {} aren't equal {}".format(
299 request_body, prepared_body
300 )
301
302 return True, ""
303
304 return match
305
306
307 def header_matcher(headers, strict_match=False):
308 """
309 Matcher to match 'headers' argument in request using the responses library.
310
311 Because ``requests`` will send several standard headers in addition to what
312 was specified by your code, request headers that are additional to the ones
313 passed to the matcher are ignored by default. You can change this behaviour
314 by passing ``strict_match=True``.
315
316 :param headers: (dict), same as provided to request
317 :param strict_match: (bool), whether headers in addition to those specified
318 in the matcher should cause the match to fail.
319 :return: (func) matcher
320 """
321
322 def match(request):
323 request_headers = request.headers or {}
324
325 if not strict_match:
326 # filter down to just the headers specified in the matcher
327 request_headers = {k: v for k, v in request_headers.items() if k in headers}
328
329 valid = sorted(headers.items()) == sorted(request_headers.items())
330
331 if not valid:
332 return False, "Headers do not match: {} doesn't match {}".format(
333 _create_key_val_str(request_headers), _create_key_val_str(headers)
334 )
335
336 return valid, ""
337
338 return match
0 from typing import (
1 Any,
2 Callable,
3 Optional,
4 Dict,
5 )
6
7 JSONDecodeError = ValueError
8
9
10 def _create_key_val_str(input_dict: Dict[Any, Any]) -> str: ...
11
12 def json_params_matcher(
13 params: Optional[Dict[str, Any]]
14 ) -> Callable[..., Any]: ...
15
16 def urlencoded_params_matcher(
17 params: Optional[Dict[str, str]]
18 ) -> Callable[..., Any]: ...
19
20 def query_param_matcher(
21 params: Optional[Dict[str, str]]
22 ) -> Callable[..., Any]: ...
23
24 def query_string_matcher(
25 query: Optional[str]
26 ) -> Callable[..., Any]: ...
27
28 def request_kwargs_matcher(
29 kwargs: Optional[Dict[str, Any]]
30 ) -> Callable[..., Any]: ...
31
32 def multipart_matcher(
33 files: Dict[str, Any], data: Optional[Dict[str, str]] = ...
34 ) -> Callable[..., Any]: ...
35
36 def header_matcher(
37 headers: Dict[str, str],
38 strict_match: bool = ...
39 ) -> Callable[..., Any]: ...
40
41 def fragment_identifier_matcher(
42 identifier: Optional[str]
43 ) -> Callable[..., Any]: ...
0 from __future__ import absolute_import, print_function, division, unicode_literals
1
2 import six
3 from sys import version_info
4
5 import pytest
6 import requests
7 import responses
8 from requests.exceptions import ConnectionError
9 from responses import matchers
10
11
12 def assert_response(resp, body=None, content_type="text/plain"):
13 assert resp.status_code == 200
14 assert resp.reason == "OK"
15 assert resp.headers["Content-Type"] == content_type
16 assert resp.text == body
17
18
19 def assert_reset():
20 assert len(responses._default_mock._matches) == 0
21 assert len(responses.calls) == 0
22
23
24 def test_query_string_matcher():
25 @responses.activate
26 def run():
27 url = "http://example.com?test=1&foo=bar"
28 responses.add(
29 responses.GET,
30 url,
31 body=b"test",
32 match=[matchers.query_string_matcher("test=1&foo=bar")],
33 )
34 resp = requests.get("http://example.com?test=1&foo=bar")
35 assert_response(resp, "test")
36 resp = requests.get("http://example.com?foo=bar&test=1")
37 assert_response(resp, "test")
38 resp = requests.get("http://example.com/?foo=bar&test=1")
39 assert_response(resp, "test")
40
41 run()
42 assert_reset()
43
44
45 def test_request_matches_post_params():
46 @responses.activate
47 def run(deprecated):
48 if deprecated:
49 json_params_matcher = getattr(responses, "json_params_matcher")
50 urlencoded_params_matcher = getattr(responses, "urlencoded_params_matcher")
51 else:
52 json_params_matcher = matchers.json_params_matcher
53 urlencoded_params_matcher = matchers.urlencoded_params_matcher
54
55 responses.add(
56 method=responses.POST,
57 url="http://example.com/",
58 body="one",
59 match=[json_params_matcher({"page": {"name": "first", "type": "json"}})],
60 )
61 responses.add(
62 method=responses.POST,
63 url="http://example.com/",
64 body="two",
65 match=[urlencoded_params_matcher({"page": "second", "type": "urlencoded"})],
66 )
67
68 resp = requests.request(
69 "POST",
70 "http://example.com/",
71 headers={"Content-Type": "x-www-form-urlencoded"},
72 data={"page": "second", "type": "urlencoded"},
73 )
74 assert_response(resp, "two")
75
76 resp = requests.request(
77 "POST",
78 "http://example.com/",
79 headers={"Content-Type": "application/json"},
80 json={"page": {"name": "first", "type": "json"}},
81 )
82 assert_response(resp, "one")
83
84 with pytest.deprecated_call():
85 run(deprecated=True)
86 assert_reset()
87
88 run(deprecated=False)
89 assert_reset()
90
91
92 def test_request_matches_empty_body():
93 def run():
94 with responses.RequestsMock(assert_all_requests_are_fired=True) as rsps:
95 # test that both json and urlencoded body are empty in matcher and in request
96 rsps.add(
97 method=responses.POST,
98 url="http://example.com/",
99 body="one",
100 match=[matchers.json_params_matcher(None)],
101 )
102
103 rsps.add(
104 method=responses.POST,
105 url="http://example.com/",
106 body="two",
107 match=[matchers.urlencoded_params_matcher(None)],
108 )
109
110 resp = requests.request("POST", "http://example.com/")
111 assert_response(resp, "one")
112
113 resp = requests.request(
114 "POST",
115 "http://example.com/",
116 headers={"Content-Type": "x-www-form-urlencoded"},
117 )
118 assert_response(resp, "two")
119
120 with responses.RequestsMock(assert_all_requests_are_fired=False) as rsps:
121 # test exception raise if matcher body is None but request data is not None
122 rsps.add(
123 method=responses.POST,
124 url="http://example.com/",
125 body="one",
126 match=[matchers.json_params_matcher(None)],
127 )
128
129 with pytest.raises(ConnectionError) as excinfo:
130 resp = requests.request(
131 "POST",
132 "http://example.com/",
133 json={"my": "data"},
134 headers={"Content-Type": "application/json"},
135 )
136
137 msg = str(excinfo.value)
138 assert "request.body doesn't match: {my: data} doesn't match {}" in msg
139
140 with responses.RequestsMock(assert_all_requests_are_fired=False) as rsps:
141 rsps.add(
142 method=responses.POST,
143 url="http://example.com/",
144 body="two",
145 match=[matchers.urlencoded_params_matcher(None)],
146 )
147 with pytest.raises(ConnectionError) as excinfo:
148 resp = requests.request(
149 "POST",
150 "http://example.com/",
151 headers={"Content-Type": "x-www-form-urlencoded"},
152 data={"page": "second", "type": "urlencoded"},
153 )
154 msg = str(excinfo.value)
155 assert (
156 "request.body doesn't match: {page: second, type: urlencoded} doesn't match {}"
157 in msg
158 )
159
160 run()
161 assert_reset()
162
163
164 def test_request_matches_params():
165 @responses.activate
166 def run():
167 url = "http://example.com/test"
168 params = {"hello": "world", "I am": "a big test"}
169 responses.add(
170 method=responses.GET,
171 url=url,
172 body="test",
173 match=[matchers.query_param_matcher(params)],
174 match_querystring=False,
175 )
176
177 # exchange parameter places for the test
178 params = {
179 "I am": "a big test",
180 "hello": "world",
181 }
182 resp = requests.get(url, params=params)
183
184 if six.PY3 and version_info[1] >= 7:
185 # only after py 3.7 dictionaries are ordered, so we can check URL
186 constructed_url = r"http://example.com/test?I+am=a+big+test&hello=world"
187 assert resp.url == constructed_url
188 assert resp.request.url == constructed_url
189
190 resp_params = getattr(resp.request, "params")
191 assert resp_params == params
192
193 run()
194 assert_reset()
195
196
197 def test_fail_matchers_error():
198 """
199 Validate that Exception is raised if request does not match responses.matchers
200 validate matchers.urlencoded_params_matcher
201 validate matchers.json_params_matcher
202 validate matchers.query_param_matcher
203 validate matchers.request_kwargs_matcher
204 :return: None
205 """
206
207 def run():
208 with responses.RequestsMock(assert_all_requests_are_fired=False) as rsps:
209 rsps.add(
210 "POST",
211 "http://example.com",
212 match=[matchers.urlencoded_params_matcher({"foo": "bar"})],
213 )
214 rsps.add(
215 "POST",
216 "http://example.com",
217 match=[matchers.json_params_matcher({"fail": "json"})],
218 )
219
220 with pytest.raises(ConnectionError) as excinfo:
221 requests.post("http://example.com", data={"id": "bad"})
222
223 msg = str(excinfo.value)
224 assert (
225 "request.body doesn't match: {id: bad} doesn't match {foo: bar}" in msg
226 )
227
228 assert (
229 "request.body doesn't match: JSONDecodeError: Cannot parse request.body"
230 in msg
231 )
232
233 with responses.RequestsMock(assert_all_requests_are_fired=False) as rsps:
234 rsps.add(
235 "GET",
236 "http://111.com",
237 match=[matchers.query_param_matcher({"my": "params"})],
238 )
239
240 rsps.add(
241 method=responses.GET,
242 url="http://111.com/",
243 body="two",
244 match=[matchers.json_params_matcher({"page": "one"})],
245 )
246
247 with pytest.raises(ConnectionError) as excinfo:
248 requests.get(
249 "http://111.com", params={"id": "bad"}, json={"page": "two"}
250 )
251
252 msg = str(excinfo.value)
253 assert (
254 "Parameters do not match. {id: bad} doesn't match {my: params}" in msg
255 )
256 assert (
257 "request.body doesn't match: {page: two} doesn't match {page: one}"
258 in msg
259 )
260
261 with responses.RequestsMock(assert_all_requests_are_fired=False) as rsps:
262 req_kwargs = {
263 "stream": True,
264 "verify": False,
265 }
266 rsps.add(
267 "GET",
268 "http://111.com",
269 match=[matchers.request_kwargs_matcher(req_kwargs)],
270 )
271
272 with pytest.raises(ConnectionError) as excinfo:
273 requests.get("http://111.com", stream=True)
274
275 msg = str(excinfo.value)
276 assert (
277 "Arguments don't match: "
278 "{stream: True, verify: True} doesn't match {stream: True, verify: False}"
279 ) in msg
280
281 run()
282 assert_reset()
283
284
285 def test_multipart_matcher():
286 @responses.activate
287 def run():
288 req_data = {"some": "other", "data": "fields"}
289 req_files = {"file_name": b"Old World!"}
290 responses.add(
291 responses.POST,
292 url="http://httpbin.org/post",
293 match=[matchers.multipart_matcher(req_files, data=req_data)],
294 )
295 resp = requests.post("http://httpbin.org/post", data=req_data, files=req_files)
296 assert resp.status_code == 200
297
298 with pytest.raises(TypeError):
299 responses.add(
300 responses.POST,
301 url="http://httpbin.org/post",
302 match=[matchers.multipart_matcher(files={})],
303 )
304
305 run()
306 assert_reset()
307
308
309 def test_multipart_matcher_fail():
310 """
311 Validate that Exception is raised if request does not match responses.matchers
312 validate matchers.multipart_matcher
313 :return: None
314 """
315
316 def run():
317 # different file contents
318 with responses.RequestsMock(assert_all_requests_are_fired=False) as rsps:
319 req_data = {"some": "other", "data": "fields"}
320 req_files = {"file_name": b"Old World!"}
321 rsps.add(
322 responses.POST,
323 url="http://httpbin.org/post",
324 match=[matchers.multipart_matcher(req_files, data=req_data)],
325 )
326
327 with pytest.raises(ConnectionError) as excinfo:
328 requests.post(
329 "http://httpbin.org/post",
330 data=req_data,
331 files={"file_name": b"New World!"},
332 )
333
334 msg = str(excinfo.value)
335 assert "multipart/form-data doesn't match. Request body differs." in msg
336 assert (
337 '\r\nContent-Disposition: form-data; name="file_name"; '
338 'filename="file_name"\r\n\r\nOld World!\r\n'
339 ) in msg
340 assert (
341 '\r\nContent-Disposition: form-data; name="file_name"; '
342 'filename="file_name"\r\n\r\nNew World!\r\n'
343 ) in msg
344
345 # x-www-form-urlencoded request
346 with responses.RequestsMock(assert_all_requests_are_fired=False) as rsps:
347 req_data = {"some": "other", "data": "fields"}
348 req_files = {"file_name": b"Old World!"}
349 rsps.add(
350 responses.POST,
351 url="http://httpbin.org/post",
352 match=[matchers.multipart_matcher(req_files, data=req_data)],
353 )
354
355 with pytest.raises(ConnectionError) as excinfo:
356 requests.post("http://httpbin.org/post", data=req_data)
357
358 msg = str(excinfo.value)
359 assert (
360 "multipart/form-data doesn't match. Request headers['Content-Type'] is different."
361 in msg
362 )
363 assert (
364 "application/x-www-form-urlencoded isn't equal to multipart/form-data; boundary="
365 in msg
366 )
367
368 # empty body request
369 with responses.RequestsMock(assert_all_requests_are_fired=False) as rsps:
370 req_files = {"file_name": b"Old World!"}
371 rsps.add(
372 responses.POST,
373 url="http://httpbin.org/post",
374 match=[matchers.multipart_matcher(req_files)],
375 )
376
377 with pytest.raises(ConnectionError) as excinfo:
378 requests.post("http://httpbin.org/post")
379
380 msg = str(excinfo.value)
381 assert "Request is missing the 'Content-Type' header" in msg
382
383 run()
384 assert_reset()
385
386
387 def test_query_string_matcher_raises():
388 """
389 Validate that Exception is raised if request does not match responses.matchers
390 validate matchers.query_string_matcher
391 :return: None
392 """
393
394 def run():
395 with responses.RequestsMock(assert_all_requests_are_fired=False) as rsps:
396 rsps.add(
397 "GET",
398 "http://111.com",
399 match=[matchers.query_string_matcher("didi=pro")],
400 )
401
402 with pytest.raises(ConnectionError) as excinfo:
403 requests.get("http://111.com", params={"test": "1", "didi": "pro"})
404
405 msg = str(excinfo.value)
406 assert (
407 "Query string doesn't match. {didi: pro, test: 1} doesn't match {didi: pro}"
408 in msg
409 )
410
411 run()
412 assert_reset()
413
414
415 def test_request_matches_headers():
416 @responses.activate
417 def run():
418 url = "http://example.com/"
419 responses.add(
420 method=responses.GET,
421 url=url,
422 json={"success": True},
423 match=[matchers.header_matcher({"Accept": "application/json"})],
424 )
425
426 responses.add(
427 method=responses.GET,
428 url=url,
429 body="success",
430 match=[matchers.header_matcher({"Accept": "text/plain"})],
431 )
432
433 # the actual request can contain extra headers (requests always adds some itself anyway)
434 resp = requests.get(
435 url, headers={"Accept": "application/json", "Accept-Charset": "utf-8"}
436 )
437 assert_response(resp, body='{"success": true}', content_type="application/json")
438
439 resp = requests.get(url, headers={"Accept": "text/plain"})
440 assert_response(resp, body="success", content_type="text/plain")
441
442 run()
443 assert_reset()
444
445
446 def test_request_matches_headers_no_match():
447 @responses.activate
448 def run():
449 url = "http://example.com/"
450 responses.add(
451 method=responses.GET,
452 url=url,
453 json={"success": True},
454 match=[matchers.header_matcher({"Accept": "application/json"})],
455 )
456
457 with pytest.raises(ConnectionError) as excinfo:
458 requests.get(url, headers={"Accept": "application/xml"})
459
460 msg = str(excinfo.value)
461 assert (
462 "Headers do not match: {Accept: application/xml} doesn't match "
463 "{Accept: application/json}"
464 ) in msg
465
466 run()
467 assert_reset()
468
469
470 def test_request_matches_headers_strict_match():
471 @responses.activate
472 def run():
473 url = "http://example.com/"
474 responses.add(
475 method=responses.GET,
476 url=url,
477 body="success",
478 match=[
479 matchers.header_matcher({"Accept": "text/plain"}, strict_match=True)
480 ],
481 )
482
483 # requests will add some extra headers of its own, so we have to use prepared requests
484 session = requests.Session()
485
486 # make sure we send *just* the header we're expectin
487 prepped = session.prepare_request(
488 requests.Request(
489 method="GET",
490 url=url,
491 )
492 )
493 prepped.headers.clear()
494 prepped.headers["Accept"] = "text/plain"
495
496 resp = session.send(prepped)
497 assert_response(resp, body="success", content_type="text/plain")
498
499 # include the "Accept-Charset" header, which will fail to match
500 prepped = session.prepare_request(
501 requests.Request(
502 method="GET",
503 url=url,
504 )
505 )
506 prepped.headers.clear()
507 prepped.headers["Accept"] = "text/plain"
508 prepped.headers["Accept-Charset"] = "utf-8"
509
510 with pytest.raises(ConnectionError) as excinfo:
511 session.send(prepped)
512
513 msg = str(excinfo.value)
514 assert (
515 "Headers do not match: {Accept: text/plain, Accept-Charset: utf-8} "
516 "doesn't match {Accept: text/plain}"
517 ) in msg
518
519 run()
520 assert_reset()
521
522
523 def test_fragment_identifier_matcher():
524 @responses.activate
525 def run():
526 responses.add(
527 responses.GET,
528 "http://example.com",
529 match=[matchers.fragment_identifier_matcher("test=1&foo=bar")],
530 body=b"test",
531 )
532
533 resp = requests.get("http://example.com#test=1&foo=bar")
534 assert_response(resp, "test")
535
536 run()
537 assert_reset()
538
539
540 def test_fragment_identifier_matcher_error():
541 @responses.activate
542 def run():
543 responses.add(
544 responses.GET,
545 "http://example.com/",
546 match=[matchers.fragment_identifier_matcher("test=1")],
547 )
548 responses.add(
549 responses.GET,
550 "http://example.com/",
551 match=[matchers.fragment_identifier_matcher(None)],
552 )
553
554 with pytest.raises(ConnectionError) as excinfo:
555 requests.get("http://example.com/#test=2")
556
557 msg = str(excinfo.value)
558 assert (
559 "URL fragment identifier is different: test=1 doesn't match test=2"
560 ) in msg
561 assert (
562 "URL fragment identifier is different: None doesn't match test=2"
563 ) in msg
564
565 run()
566 assert_reset()
567
568
569 def test_fragment_identifier_matcher_and_match_querystring():
570 @responses.activate
571 def run():
572 url = "http://example.com?ab=xy&zed=qwe#test=1&foo=bar"
573 responses.add(
574 responses.GET,
575 url,
576 match_querystring=True,
577 match=[matchers.fragment_identifier_matcher("test=1&foo=bar")],
578 body=b"test",
579 )
580
581 # two requests to check reversed order of fragment identifier
582 resp = requests.get("http://example.com?ab=xy&zed=qwe#test=1&foo=bar")
583 assert_response(resp, "test")
584 resp = requests.get("http://example.com?zed=qwe&ab=xy#foo=bar&test=1")
585 assert_response(resp, "test")
586
587 run()
588 assert_reset()
589
590
591 def test_matchers_create_key_val_str():
592 """
593 Test that matchers._create_key_val_str does recursive conversion
594 """
595 data = {
596 "my_list": [
597 1,
598 2,
599 "a",
600 {"key1": "val1", "key2": 2, 3: "test"},
601 "!",
602 [["list", "nested"], {"nested": "dict"}],
603 ],
604 1: 4,
605 "test": "val",
606 "high": {"nested": "nested_dict"},
607 }
608 conv_str = matchers._create_key_val_str(data)
609 reference = (
610 "{1: 4, high: {nested: nested_dict}, my_list: [!, 1, 2, [[list, nested], {nested: dict}], "
611 "a, {3: test, key1: val1, key2: 2}], test: val}"
612 )
613 assert conv_str == reference
22 from __future__ import absolute_import, print_function, division, unicode_literals
33
44 import inspect
5 import os
56 import re
67 import six
78 from io import BufferedReader, BytesIO
910 import pytest
1011 import requests
1112 import responses
12 from requests.exceptions import ConnectionError, HTTPError
13 from responses import BaseResponse, Response
13 from requests.exceptions import ConnectionError, HTTPError, ChunkedEncodingError
14 from responses import (
15 BaseResponse,
16 Response,
17 PassthroughResponse,
18 matchers,
19 CallbackResponse,
20 )
21
1422
1523 try:
1624 from mock import patch, Mock
8997 assert_response(resp, "")
9098 assert len(responses.calls) == 2
9199 assert responses.calls[1].request.url == "http://example.com/?foo=bar"
100
101 run()
102 assert_reset()
92103
93104
94105 @pytest.mark.parametrize(
330341 assert_reset()
331342
332343
333 def test_match_empty_querystring():
344 def test_match_querystring_empty():
334345 @responses.activate
335346 def run():
336347 responses.add(
529540 }
530541 url = "http://example.com/"
531542
532 def request_callback(request):
533 return (status, headers, body)
543 def request_callback(_request):
544 return status, headers, body
534545
535546 @responses.activate
536547 def run():
547558 assert_reset()
548559
549560
561 def test_callback_deprecated_argument():
562 with pytest.deprecated_call():
563 CallbackResponse(responses.GET, "url", lambda x: x, stream=False)
564
565
550566 def test_callback_exception_result():
551567 result = Exception()
552568 url = "http://example.com/"
572588 url = "http://example.com/"
573589
574590 def request_callback(request):
575 return (200, {}, body)
591 return 200, {}, body
576592
577593 @responses.activate
578594 def run():
594610 headers = {"foo": "bar"}
595611 url = "http://example.com/"
596612
597 def request_callback(request):
598 return (status, headers, body)
613 def request_callback(_request):
614 return status, headers, body
599615
600616 @responses.activate
601617 def run():
630646 assert_reset()
631647
632648
649 def test_callback_matchers():
650 def request_callback(request):
651 return (
652 200,
653 {"Content-Type": "application/json"},
654 b"foo",
655 )
656
657 @responses.activate
658 def run():
659 req_data = {"some": "other", "data": "fields"}
660 req_files = {"file_name": b"Old World!"}
661
662 responses.add_callback(
663 responses.POST,
664 url="http://httpbin.org/post",
665 match=[matchers.multipart_matcher(req_files, data=req_data)],
666 callback=request_callback,
667 )
668 resp = requests.post("http://httpbin.org/post", data=req_data, files=req_files)
669 assert resp.text == "foo"
670 assert resp.headers["content-type"] == "application/json"
671
672 run()
673 assert_reset()
674
675
676 def test_callback_matchers_fail():
677 @responses.activate
678 def run():
679 req_data = {"some": "other", "data": "fields"}
680 req_files = {"file_name": b"Old World!"}
681
682 responses.add_callback(
683 responses.POST,
684 url="http://httpbin.org/post",
685 match=[matchers.multipart_matcher(req_files, data=req_data)],
686 callback=lambda x: (
687 0,
688 {"a": ""},
689 "",
690 ),
691 )
692 with pytest.raises(ConnectionError) as exc:
693 requests.post(
694 "http://httpbin.org/post",
695 data={"some": "other", "data": "wrong"},
696 files=req_files,
697 )
698
699 assert "multipart/form-data doesn't match." in str(exc.value)
700
701 run()
702 assert_reset()
703
704
633705 def test_callback_content_type_tuple():
634706 def request_callback(request):
635707 return (
669741
670742 run()
671743 assert_reset()
744
745
746 def test_base_response_get_response():
747 resp = BaseResponse("GET", ".com")
748 with pytest.raises(NotImplementedError):
749 resp.get_response(requests.PreparedRequest())
672750
673751
674752 def test_custom_adapter():
738816 assert decorated_test_function(3) == test_function(3)
739817
740818
819 @pytest.fixture
820 def my_fruit():
821 return "apple"
822
823
824 @pytest.fixture
825 def fruit_basket(my_fruit):
826 return ["banana", my_fruit]
827
828
829 @pytest.mark.usefixtures("my_fruit", "fruit_basket")
830 class TestFixtures(object):
831 """
832 Test that pytest fixtures work well with 'activate' decorator
833 """
834
835 def test_function(self, my_fruit, fruit_basket):
836 assert my_fruit in fruit_basket
837 assert my_fruit == "apple"
838
839 test_function_decorated = responses.activate(test_function)
840
841
741842 def test_activate_mock_interaction():
742843 @patch("sys.stdout")
743844 def test_function(mock_stdout):
763864 @pytest.mark.skipif(six.PY2, reason="Cannot run in python2")
764865 def test_activate_doesnt_change_signature_with_return_type():
765866 def test_function(a, b=None):
766 return (a, b)
867 return a, b
767868
768869 # Add type annotations as they are syntax errors in py2.
769870 # Use a class to test for import errors in evaled code.
771872 test_function.__annotations__["a"] = Mock
772873
773874 decorated_test_function = responses.activate(test_function)
774 if hasattr(inspect, "signature"):
775 assert inspect.signature(test_function) == inspect.signature(
776 decorated_test_function
777 )
778 else:
779 assert inspect.getargspec(test_function) == inspect.getargspec(
780 decorated_test_function
781 )
875 assert inspect.signature(test_function) == inspect.signature(
876 decorated_test_function
877 )
878
782879 assert decorated_test_function(1, 2) == test_function(1, 2)
783880 assert decorated_test_function(3) == test_function(3)
784881
818915 assert_reset()
819916
820917
821 def test_response_secure_cookies():
918 def test_response_cookies_secure():
822919 body = b"test callback"
823920 status = 200
824921 headers = {"set-cookie": "session_id=12345; a=b; c=d; secure"}
867964 assert_reset()
868965
869966
967 @pytest.mark.parametrize("request_stream", (True, False, None))
968 @pytest.mark.parametrize("responses_stream", (True, False, None))
969 def test_response_cookies_session(request_stream, responses_stream):
970 @responses.activate
971 def run():
972 url = "https://example.com/path"
973 responses.add(
974 responses.GET,
975 url,
976 headers=[
977 ("Set-cookie", "mycookie=cookieval; path=/; secure"),
978 ],
979 body="ok",
980 stream=responses_stream,
981 )
982 session = requests.session()
983 resp = session.get(url, stream=request_stream)
984 assert resp.text == "ok"
985 assert resp.status_code == 200
986
987 assert "mycookie" in resp.cookies
988 assert resp.cookies["mycookie"] == "cookieval"
989 assert set(resp.cookies.keys()) == set(["mycookie"])
990
991 assert "mycookie" in session.cookies
992 assert session.cookies["mycookie"] == "cookieval"
993 assert set(session.cookies.keys()) == set(["mycookie"])
994
995 run()
996 assert_reset()
997
998
870999 def test_response_callback():
8711000 """adds a callback to decorate the response, then checks it"""
8721001
8861015 assert_reset()
8871016
8881017
1018 @pytest.mark.skipif(six.PY2, reason="re.compile works differntly in PY2")
8891019 def test_response_filebody():
8901020 """ Adds the possibility to use actual (binary) files as responses """
8911021
8921022 def run():
1023 current_file = os.path.abspath(__file__)
8931024 with responses.RequestsMock() as m:
894 with open("README.rst", "r") as out:
1025 with open(current_file, "r") as out:
8951026 m.add(responses.GET, "http://example.com", body=out.read(), stream=True)
896 resp = requests.get("http://example.com")
897 with open("README.rst", "r") as out:
1027 resp = requests.get("http://example.com", stream=True)
1028 with open(current_file, "r") as out:
8981029 assert resp.text == out.read()
1030
1031 run()
1032 assert_reset()
1033
1034
1035 def test_use_stream_twice_to_double_raw_io():
1036 @responses.activate
1037 def run():
1038 url = "http://example.com"
1039 responses.add(responses.GET, url, body=b"42", stream=True)
1040 resp = requests.get(url, stream=True)
1041 assert resp.raw.read() == b"42"
1042
1043 run()
1044 assert_reset()
8991045
9001046
9011047 def test_assert_all_requests_are_fired():
10641210 assert_reset()
10651211
10661212
1213 def test_content_length_error(monkeypatch):
1214 """
1215 Currently 'requests' does not enforce content length validation,
1216 (validation that body length matches header). However, this could
1217 be expected in next major version, see
1218 https://github.com/psf/requests/pull/3563
1219
1220 Now user can manually patch URL3 lib to achieve the same
1221 """
1222
1223 @responses.activate
1224 def run():
1225 responses.add(
1226 responses.GET,
1227 "http://example.com/api/123",
1228 json={"message": "this body is too large"},
1229 adding_headers={"content-length": "2"},
1230 )
1231 with pytest.raises(ChunkedEncodingError) as exc:
1232 requests.get("http://example.com/api/123")
1233
1234 assert "IncompleteRead" in str(exc.value)
1235
1236 original_init = getattr(requests.packages.urllib3.HTTPResponse, "__init__")
1237
1238 def patched_init(self, *args, **kwargs):
1239 kwargs["enforce_content_length"] = True
1240 original_init(self, *args, **kwargs)
1241
1242 monkeypatch.setattr(
1243 requests.packages.urllib3.HTTPResponse, "__init__", patched_init
1244 )
1245
1246 run()
1247 assert_reset()
1248
1249
10671250 def test_legacy_adding_headers():
10681251 @responses.activate
10691252 def run():
10751258 )
10761259 resp = requests.get("http://example.com")
10771260 assert resp.headers["X-Test"] == "foo"
1261
1262 run()
1263 assert_reset()
1264
1265
1266 def test_auto_calculate_content_length_string_body():
1267 @responses.activate
1268 def run():
1269 url = "http://example.com/"
1270 responses.add(
1271 responses.GET, url, body="test", auto_calculate_content_length=True
1272 )
1273 resp = requests.get(url)
1274 assert_response(resp, "test")
1275 assert resp.headers["Content-Length"] == "4"
1276
1277 run()
1278 assert_reset()
1279
1280
1281 def test_auto_calculate_content_length_bytes_body():
1282 @responses.activate
1283 def run():
1284 url = "http://example.com/"
1285 responses.add(
1286 responses.GET, url, body=b"test bytes", auto_calculate_content_length=True
1287 )
1288 resp = requests.get(url)
1289 assert_response(resp, "test bytes")
1290 assert resp.headers["Content-Length"] == "10"
1291
1292 run()
1293 assert_reset()
1294
1295
1296 def test_auto_calculate_content_length_json_body():
1297 @responses.activate
1298 def run():
1299 content_type = "application/json"
1300
1301 url = "http://example.com/"
1302 responses.add(
1303 responses.GET,
1304 url,
1305 json={"message": "success"},
1306 auto_calculate_content_length=True,
1307 )
1308 resp = requests.get(url)
1309 assert_response(resp, '{"message": "success"}', content_type)
1310 assert resp.headers["Content-Length"] == "22"
1311
1312 url = "http://example.com/1/"
1313 responses.add(responses.GET, url, json=[], auto_calculate_content_length=True)
1314 resp = requests.get(url)
1315 assert_response(resp, "[]", content_type)
1316 assert resp.headers["Content-Length"] == "2"
1317
1318 run()
1319 assert_reset()
1320
1321
1322 def test_auto_calculate_content_length_unicode_body():
1323 @responses.activate
1324 def run():
1325 url = "http://example.com/test"
1326 responses.add(
1327 responses.GET, url, body="михољско лето", auto_calculate_content_length=True
1328 )
1329 resp = requests.get(url)
1330 assert_response(resp, "михољско лето", content_type="text/plain; charset=utf-8")
1331 assert resp.headers["Content-Length"] == "25"
1332
1333 run()
1334 assert_reset()
1335
1336
1337 def test_auto_calculate_content_length_doesnt_work_for_buffered_reader_body():
1338 @responses.activate
1339 def run():
1340 url = "http://example.com/test"
1341 responses.add(
1342 responses.GET,
1343 url,
1344 body=BufferedReader(BytesIO(b"testing")), # type: ignore
1345 auto_calculate_content_length=True,
1346 )
1347 resp = requests.get(url)
1348 assert_response(resp, "testing")
1349 assert "Content-Length" not in resp.headers
1350
1351 run()
1352 assert_reset()
1353
1354
1355 def test_auto_calculate_content_length_doesnt_override_existing_value():
1356 @responses.activate
1357 def run():
1358 url = "http://example.com/"
1359 responses.add(
1360 responses.GET,
1361 url,
1362 body="test",
1363 headers={"Content-Length": "2"},
1364 auto_calculate_content_length=True,
1365 )
1366 resp = requests.get(url)
1367 assert_response(resp, "test")
1368 assert resp.headers["Content-Length"] == "2"
10781369
10791370 run()
10801371 assert_reset()
11251416 assert_response(resp, "posted")
11261417
11271418 run()
1419 assert_reset()
1420
1421
1422 def test_passthrough_flag(httpserver):
1423 httpserver.serve_content("OK", headers={"Content-Type": "text/plain"})
1424 response = Response(responses.GET, httpserver.url, body="MOCK")
1425
1426 @responses.activate
1427 def run_passthrough():
1428 responses.add(response)
1429 resp = requests.get(httpserver.url)
1430 assert_response(resp, "OK")
1431
1432 @responses.activate
1433 def run_mocked():
1434 responses.add(response)
1435 resp = requests.get(httpserver.url)
1436 assert_response(resp, "MOCK")
1437
1438 run_mocked()
1439 assert_reset()
1440
1441 response.passthrough = True
1442 run_passthrough()
1443 assert_reset()
1444
1445
1446 def test_passthrough_response(httpserver):
1447 httpserver.serve_content("OK", headers={"Content-Type": "text/plain"})
1448
1449 @responses.activate
1450 def run():
1451 responses.add(PassthroughResponse(responses.GET, httpserver.url))
1452 responses.add(responses.GET, "{}/one".format(httpserver.url), body="one")
1453 responses.add(responses.GET, "http://example.com/two", body="two")
1454
1455 resp = requests.get("http://example.com/two")
1456 assert_response(resp, "two")
1457 resp = requests.get("{}/one".format(httpserver.url))
1458 assert_response(resp, "one")
1459 resp = requests.get(httpserver.url)
1460 assert_response(resp, "OK")
1461
1462 assert len(responses.calls) == 3
1463 responses.assert_call_count(httpserver.url, 1)
1464
1465 run()
1466 assert_reset()
1467
1468
1469 def test_passthrough_response_stream(httpserver):
1470 httpserver.serve_content("OK", headers={"Content-Type": "text/plain"})
1471
1472 @responses.activate
1473 def run():
1474 responses.add(PassthroughResponse(responses.GET, httpserver.url))
1475 content_1 = requests.get(httpserver.url).content
1476 with requests.get(httpserver.url, stream=True) as resp:
1477 content_2 = resp.raw.read()
1478 assert content_1 == content_2
1479
1480 run()
1481 assert_reset()
1482
1483
1484 def test_passthru_prefixes(httpserver):
1485 httpserver.serve_content("OK", headers={"Content-Type": "text/plain"})
1486
1487 @responses.activate
1488 def run_constructor_argument():
1489 with responses.RequestsMock(passthru_prefixes=(httpserver.url,)):
1490 resp = requests.get(httpserver.url)
1491 assert_response(resp, "OK")
1492
1493 @responses.activate
1494 def run_property_setter():
1495 with responses.RequestsMock() as m:
1496 m.passthru_prefixes = tuple([httpserver.url])
1497 resp = requests.get(httpserver.url)
1498 assert_response(resp, "OK")
1499
1500 run_constructor_argument()
1501 assert_reset()
1502 run_property_setter()
11281503 assert_reset()
11291504
11301505
13011676 assert_reset()
13021677
13031678
1304 def test_request_matches_post_params():
1305 @responses.activate
1306 def run():
1307 responses.add(
1308 method=responses.POST,
1309 url="http://example.com/",
1310 body="one",
1311 match=[
1312 responses.json_params_matcher(
1313 {"page": {"name": "first", "type": "json"}}
1314 )
1315 ],
1316 )
1317 responses.add(
1318 method=responses.POST,
1319 url="http://example.com/",
1320 body="two",
1321 match=[
1322 responses.urlencoded_params_matcher(
1323 {"page": "second", "type": "urlencoded"}
1324 )
1325 ],
1326 )
1327
1328 resp = requests.request(
1329 "POST",
1330 "http://example.com/",
1331 headers={"Content-Type": "x-www-form-urlencoded"},
1332 data={"page": "second", "type": "urlencoded"},
1333 )
1334 assert_response(resp, "two")
1335
1336 resp = requests.request(
1337 "POST",
1338 "http://example.com/",
1339 headers={"Content-Type": "application/json"},
1340 json={"page": {"name": "first", "type": "json"}},
1341 )
1342 assert_response(resp, "one")
1343
1344 run()
1345 assert_reset()
1346
1347
1348 def test_request_matches_empty_body():
1349 @responses.activate
1350 def run():
1351 responses.add(
1352 method=responses.POST,
1353 url="http://example.com/",
1354 body="one",
1355 match=[responses.json_params_matcher(None)],
1356 )
1357
1358 responses.add(
1359 method=responses.POST,
1360 url="http://example.com/",
1361 body="two",
1362 match=[responses.urlencoded_params_matcher(None)],
1363 )
1364
1365 resp = requests.request("POST", "http://example.com/")
1366 assert_response(resp, "one")
1367
1368 resp = requests.request(
1369 "POST",
1370 "http://example.com/",
1371 headers={"Content-Type": "x-www-form-urlencoded"},
1372 )
1373 assert_response(resp, "two")
1374
1375 run()
1376 assert_reset()
1377
1378
13791679 def test_fail_request_error():
1380 @responses.activate
1381 def run():
1382 responses.add("POST", "http://example1.com")
1383 responses.add("GET", "http://example.com")
1384 responses.add(
1385 "POST",
1386 "http://example.com",
1387 match=[responses.urlencoded_params_matcher({"foo": "bar"})],
1388 )
1389
1390 with pytest.raises(ConnectionError) as excinfo:
1391 requests.post("http://example.com", data={"id": "bad"})
1392 msg = str(excinfo.value)
1393 assert "- POST http://example1.com/ URL does not match" in msg
1394 assert "- GET http://example.com/ Method does not match" in msg
1395 assert "- POST http://example.com/ Parameters do not match" in msg
1680 """
1681 Validate that exception is raised if request URL/Method/kwargs don't match
1682 :return:
1683 """
1684
1685 def run():
1686 with responses.RequestsMock(assert_all_requests_are_fired=False) as rsps:
1687 rsps.add("POST", "http://example1.com")
1688 rsps.add("GET", "http://example.com")
1689
1690 with pytest.raises(ConnectionError) as excinfo:
1691 requests.post("http://example.com", data={"id": "bad"})
1692
1693 msg = str(excinfo.value)
1694 assert "- POST http://example1.com/ URL does not match" in msg
1695 assert "- GET http://example.com/ Method does not match" in msg
13961696
13971697 run()
13981698 assert_reset()
00 Metadata-Version: 2.1
11 Name: responses
2 Version: 0.13.4
2 Version: 0.14.0
33 Summary: A utility library for mocking out the `requests` Python library.
44 Home-page: https://github.com/getsentry/responses
55 Author: David Cramer
66 License: Apache 2.0
7 Description: Responses
8 =========
9
10 .. image:: https://img.shields.io/pypi/v/responses.svg
11 :target: https://pypi.python.org/pypi/responses/
12
13 .. image:: https://travis-ci.org/getsentry/responses.svg?branch=master
14 :target: https://travis-ci.org/getsentry/responses
15
16 .. image:: https://img.shields.io/pypi/pyversions/responses.svg
17 :target: https://pypi.org/project/responses/
18
19 A utility library for mocking out the ``requests`` Python library.
20
21 .. note::
22
23 Responses requires Python 2.7 or newer, and requests >= 2.0
24
25
26 Installing
27 ----------
28
29 ``pip install responses``
30
31
32 Basics
33 ------
34
35 The core of ``responses`` comes from registering mock responses:
36
37 .. code-block:: python
38
39 import responses
40 import requests
41
42 @responses.activate
43 def test_simple():
44 responses.add(responses.GET, 'http://twitter.com/api/1/foobar',
45 json={'error': 'not found'}, status=404)
46
47 resp = requests.get('http://twitter.com/api/1/foobar')
48
49 assert resp.json() == {"error": "not found"}
50
51 assert len(responses.calls) == 1
52 assert responses.calls[0].request.url == 'http://twitter.com/api/1/foobar'
53 assert responses.calls[0].response.text == '{"error": "not found"}'
54
55 If you attempt to fetch a url which doesn't hit a match, ``responses`` will raise
56 a ``ConnectionError``:
57
58 .. code-block:: python
59
60 import responses
61 import requests
62
63 from requests.exceptions import ConnectionError
64
65 @responses.activate
66 def test_simple():
67 with pytest.raises(ConnectionError):
68 requests.get('http://twitter.com/api/1/foobar')
69
70 Lastly, you can pass an ``Exception`` as the body to trigger an error on the request:
71
72 .. code-block:: python
73
74 import responses
75 import requests
76
77 @responses.activate
78 def test_simple():
79 responses.add(responses.GET, 'http://twitter.com/api/1/foobar',
80 body=Exception('...'))
81 with pytest.raises(Exception):
82 requests.get('http://twitter.com/api/1/foobar')
83
84
85 Response Parameters
86 -------------------
87
88 Responses are automatically registered via params on ``add``, but can also be
89 passed directly:
90
91 .. code-block:: python
92
93 import responses
94
95 responses.add(
96 responses.Response(
97 method='GET',
98 url='http://example.com',
99 )
100 )
101
102 The following attributes can be passed to a Response mock:
103
104 method (``str``)
105 The HTTP method (GET, POST, etc).
106
107 url (``str`` or compiled regular expression)
108 The full resource URL.
109
110 match_querystring (``bool``)
111 Include the query string when matching requests.
112 Enabled by default if the response URL contains a query string,
113 disabled if it doesn't or the URL is a regular expression.
114
115 body (``str`` or ``BufferedReader``)
116 The response body.
117
118 json
119 A Python object representing the JSON response body. Automatically configures
120 the appropriate Content-Type.
121
122 status (``int``)
123 The HTTP status code.
124
125 content_type (``content_type``)
126 Defaults to ``text/plain``.
127
128 headers (``dict``)
129 Response headers.
130
131 stream (``bool``)
132 Disabled by default. Indicates the response should use the streaming API.
133
134 match (``list``)
135 A list of callbacks to match requests based on request body contents.
136
137
138 Matching Request Parameters
139 ---------------------------
140
141 When adding responses for endpoints that are sent request data you can add
142 matchers to ensure your code is sending the right parameters and provide
143 different responses based on the request body contents. Responses provides
144 matchers for JSON and URLencoded request bodies and you can supply your own for
145 other formats.
146
147 .. code-block:: python
148
149 import responses
150 import requests
151
152 @responses.activate
153 def test_calc_api():
154 responses.add(
155 responses.POST,
156 url='http://calc.com/sum',
157 body="4",
158 match=[
159 responses.urlencoded_params_matcher({"left": "1", "right": "3"})
160 ]
161 )
162 requests.post("http://calc.com/sum", data={"left": 1, "right": 3})
163
164 Matching JSON encoded data can be done with ``responses.json_params_matcher()``.
165 If your application uses other encodings you can build your own matcher that
166 returns ``True`` or ``False`` if the request parameters match. Your matcher can
167 expect a ``request_body`` parameter to be provided by responses.
168
169 Dynamic Responses
170 -----------------
171
172 You can utilize callbacks to provide dynamic responses. The callback must return
173 a tuple of (``status``, ``headers``, ``body``).
174
175 .. code-block:: python
176
177 import json
178
179 import responses
180 import requests
181
182 @responses.activate
183 def test_calc_api():
184
185 def request_callback(request):
186 payload = json.loads(request.body)
187 resp_body = {'value': sum(payload['numbers'])}
188 headers = {'request-id': '728d329e-0e86-11e4-a748-0c84dc037c13'}
189 return (200, headers, json.dumps(resp_body))
190
191 responses.add_callback(
192 responses.POST, 'http://calc.com/sum',
193 callback=request_callback,
194 content_type='application/json',
195 )
196
197 resp = requests.post(
198 'http://calc.com/sum',
199 json.dumps({'numbers': [1, 2, 3]}),
200 headers={'content-type': 'application/json'},
201 )
202
203 assert resp.json() == {'value': 6}
204
205 assert len(responses.calls) == 1
206 assert responses.calls[0].request.url == 'http://calc.com/sum'
207 assert responses.calls[0].response.text == '{"value": 6}'
208 assert (
209 responses.calls[0].response.headers['request-id'] ==
210 '728d329e-0e86-11e4-a748-0c84dc037c13'
211 )
212
213 You can also pass a compiled regex to ``add_callback`` to match multiple urls:
214
215 .. code-block:: python
216
217 import re, json
218
219 from functools import reduce
220
221 import responses
222 import requests
223
224 operators = {
225 'sum': lambda x, y: x+y,
226 'prod': lambda x, y: x*y,
227 'pow': lambda x, y: x**y
228 }
229
230 @responses.activate
231 def test_regex_url():
232
233 def request_callback(request):
234 payload = json.loads(request.body)
235 operator_name = request.path_url[1:]
236
237 operator = operators[operator_name]
238
239 resp_body = {'value': reduce(operator, payload['numbers'])}
240 headers = {'request-id': '728d329e-0e86-11e4-a748-0c84dc037c13'}
241 return (200, headers, json.dumps(resp_body))
242
243 responses.add_callback(
244 responses.POST,
245 re.compile('http://calc.com/(sum|prod|pow|unsupported)'),
246 callback=request_callback,
247 content_type='application/json',
248 )
249
250 resp = requests.post(
251 'http://calc.com/prod',
252 json.dumps({'numbers': [2, 3, 4]}),
253 headers={'content-type': 'application/json'},
254 )
255 assert resp.json() == {'value': 24}
256
257 test_regex_url()
258
259
260 If you want to pass extra keyword arguments to the callback function, for example when reusing
261 a callback function to give a slightly different result, you can use ``functools.partial``:
262
263 .. code-block:: python
264
265 from functools import partial
266
267 ...
268
269 def request_callback(request, id=None):
270 payload = json.loads(request.body)
271 resp_body = {'value': sum(payload['numbers'])}
272 headers = {'request-id': id}
273 return (200, headers, json.dumps(resp_body))
274
275 responses.add_callback(
276 responses.POST, 'http://calc.com/sum',
277 callback=partial(request_callback, id='728d329e-0e86-11e4-a748-0c84dc037c13'),
278 content_type='application/json',
279 )
280
281
282 You can see params passed in the original ``request`` in ``responses.calls[].request.params``:
283
284 .. code-block:: python
285
286 import responses
287 import requests
288
289 @responses.activate
290 def test_request_params():
291 responses.add(
292 method=responses.GET,
293 url="http://example.com?hello=world",
294 body="test",
295 match_querystring=False,
296 )
297
298 resp = requests.get('http://example.com', params={"hello": "world"})
299 assert responses.calls[0].request.params == {"hello": "world"}
300
301 Responses as a context manager
302 ------------------------------
303
304 .. code-block:: python
305
306 import responses
307 import requests
308
309 def test_my_api():
310 with responses.RequestsMock() as rsps:
311 rsps.add(responses.GET, 'http://twitter.com/api/1/foobar',
312 body='{}', status=200,
313 content_type='application/json')
314 resp = requests.get('http://twitter.com/api/1/foobar')
315
316 assert resp.status_code == 200
317
318 # outside the context manager requests will hit the remote server
319 resp = requests.get('http://twitter.com/api/1/foobar')
320 resp.status_code == 404
321
322 Responses as a pytest fixture
323 -----------------------------
324
325 .. code-block:: python
326
327 @pytest.fixture
328 def mocked_responses():
329 with responses.RequestsMock() as rsps:
330 yield rsps
331
332 def test_api(mocked_responses):
333 mocked_responses.add(
334 responses.GET, 'http://twitter.com/api/1/foobar',
335 body='{}', status=200,
336 content_type='application/json')
337 resp = requests.get('http://twitter.com/api/1/foobar')
338 assert resp.status_code == 200
339
340 Responses inside a unittest setUp()
341 -----------------------------------
342
343 When run with unittest tests, this can be used to set up some
344 generic class-level responses, that may be complemented by each test
345
346 .. code-block:: python
347
348 def setUp():
349 self.responses = responses.RequestsMock()
350 self.responses.start()
351
352 # self.responses.add(...)
353
354 self.addCleanup(self.responses.stop)
355 self.addCleanup(self.responses.reset)
356
357 def test_api(self):
358 self.responses.add(
359 responses.GET, 'http://twitter.com/api/1/foobar',
360 body='{}', status=200,
361 content_type='application/json')
362 resp = requests.get('http://twitter.com/api/1/foobar')
363 assert resp.status_code == 200
364
365 Assertions on declared responses
366 --------------------------------
367
368 When used as a context manager, Responses will, by default, raise an assertion
369 error if a url was registered but not accessed. This can be disabled by passing
370 the ``assert_all_requests_are_fired`` value:
371
372 .. code-block:: python
373
374 import responses
375 import requests
376
377 def test_my_api():
378 with responses.RequestsMock(assert_all_requests_are_fired=False) as rsps:
379 rsps.add(responses.GET, 'http://twitter.com/api/1/foobar',
380 body='{}', status=200,
381 content_type='application/json')
382
383 assert_call_count
384 -----------------
385
386 Assert that the request was called exactly n times.
387
388 .. code-block:: python
389
390 import responses
391 import requests
392
393 @responses.activate
394 def test_assert_call_count():
395 responses.add(responses.GET, "http://example.com")
396
397 requests.get("http://example.com")
398 assert responses.assert_call_count("http://example.com", 1) is True
399
400 requests.get("http://example.com")
401 with pytest.raises(AssertionError) as excinfo:
402 responses.assert_call_count("http://example.com", 1)
403 assert "Expected URL 'http://example.com' to be called 1 times. Called 2 times." in str(excinfo.value)
404
405
406 Multiple Responses
407 ------------------
408
409 You can also add multiple responses for the same url:
410
411 .. code-block:: python
412
413 import responses
414 import requests
415
416 @responses.activate
417 def test_my_api():
418 responses.add(responses.GET, 'http://twitter.com/api/1/foobar', status=500)
419 responses.add(responses.GET, 'http://twitter.com/api/1/foobar',
420 body='{}', status=200,
421 content_type='application/json')
422
423 resp = requests.get('http://twitter.com/api/1/foobar')
424 assert resp.status_code == 500
425 resp = requests.get('http://twitter.com/api/1/foobar')
426 assert resp.status_code == 200
427
428
429 Using a callback to modify the response
430 ---------------------------------------
431
432 If you use customized processing in `requests` via subclassing/mixins, or if you
433 have library tools that interact with `requests` at a low level, you may need
434 to add extended processing to the mocked Response object to fully simulate the
435 environment for your tests. A `response_callback` can be used, which will be
436 wrapped by the library before being returned to the caller. The callback
437 accepts a `response` as it's single argument, and is expected to return a
438 single `response` object.
439
440 .. code-block:: python
441
442 import responses
443 import requests
444
445 def response_callback(resp):
446 resp.callback_processed = True
447 return resp
448
449 with responses.RequestsMock(response_callback=response_callback) as m:
450 m.add(responses.GET, 'http://example.com', body=b'test')
451 resp = requests.get('http://example.com')
452 assert resp.text == "test"
453 assert hasattr(resp, 'callback_processed')
454 assert resp.callback_processed is True
455
456
457 Passing through real requests
458 -----------------------------
459
460 In some cases you may wish to allow for certain requests to pass through responses
461 and hit a real server. This can be done with the ``add_passthru`` methods:
462
463 .. code-block:: python
464
465 import responses
466
467 @responses.activate
468 def test_my_api():
469 responses.add_passthru('https://percy.io')
470
471 This will allow any requests matching that prefix, that is otherwise not registered
472 as a mock response, to passthru using the standard behavior.
473
474 Regex can be used like:
475
476 .. code-block:: python
477
478 responses.add_passthru(re.compile('https://percy.io/\\w+'))
479
480
481 Viewing/Modifying registered responses
482 --------------------------------------
483
484 Registered responses are available as a public method of the RequestMock
485 instance. It is sometimes useful for debugging purposes to view the stack of
486 registered responses which can be accessed via ``responses.registered()``.
487
488 The ``replace`` function allows a previously registered ``response`` to be
489 changed. The method signature is identical to ``add``. ``response`` s are
490 identified using ``method`` and ``url``. Only the first matched ``response`` is
491 replaced.
492
493 .. code-block:: python
494
495 import responses
496 import requests
497
498 @responses.activate
499 def test_replace():
500
501 responses.add(responses.GET, 'http://example.org', json={'data': 1})
502 responses.replace(responses.GET, 'http://example.org', json={'data': 2})
503
504 resp = requests.get('http://example.org')
505
506 assert resp.json() == {'data': 2}
507
508
509 The ``upsert`` function allows a previously registered ``response`` to be
510 changed like ``replace``. If the response is registered, the ``upsert`` function
511 will registered it like ``add``.
512
513 ``remove`` takes a ``method`` and ``url`` argument and will remove **all**
514 matched responses from the registered list.
515
516 Finally, ``reset`` will reset all registered responses.
517
518 Contributing
519 ------------
520
521 Responses uses several linting and autoformatting utilities, so it's important that when
522 submitting patches you use the appropriate toolchain:
523
524 Clone the repository:
525
526 .. code-block:: shell
527
528 git clone https://github.com/getsentry/responses.git
529
530 Create an environment (e.g. with ``virtualenv``):
531
532 .. code-block:: shell
533
534 virtualenv .env && source .env/bin/activate
535
536 Configure development requirements:
537
538 .. code-block:: shell
539
540 make develop
541
542 Responses uses `Pytest <https://docs.pytest.org/en/latest/>`_ for
543 testing. You can run all tests by:
544
545 .. code-block:: shell
546
547 pytest
548
549 And run a single test by:
550
551 .. code-block:: shell
552
553 pytest -k '<test_function_name>'
554
5557 Platform: UNKNOWN
5568 Classifier: Intended Audience :: Developers
5579 Classifier: Intended Audience :: System Administrators
56517 Classifier: Programming Language :: Python :: 3.7
56618 Classifier: Programming Language :: Python :: 3.8
56719 Classifier: Programming Language :: Python :: 3.9
20 Classifier: Programming Language :: Python :: 3.10
56821 Classifier: Topic :: Software Development
56922 Requires-Python: >=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*
57023 Description-Content-Type: text/x-rst
57124 Provides-Extra: tests
25 License-File: LICENSE
26
27 Responses
28 =========
29
30 .. image:: https://img.shields.io/pypi/v/responses.svg
31 :target: https://pypi.python.org/pypi/responses/
32
33 .. image:: https://img.shields.io/pypi/pyversions/responses.svg
34 :target: https://pypi.org/project/responses/
35
36 .. image:: https://codecov.io/gh/getsentry/responses/branch/master/graph/badge.svg
37 :target: https://codecov.io/gh/getsentry/responses/
38
39 A utility library for mocking out the ``requests`` Python library.
40
41 .. note::
42
43 Responses requires Python 2.7 or newer, and requests >= 2.0
44
45
46 Installing
47 ----------
48
49 ``pip install responses``
50
51
52 Basics
53 ------
54
55 The core of ``responses`` comes from registering mock responses:
56
57 .. code-block:: python
58
59 import responses
60 import requests
61
62 @responses.activate
63 def test_simple():
64 responses.add(responses.GET, 'http://twitter.com/api/1/foobar',
65 json={'error': 'not found'}, status=404)
66
67 resp = requests.get('http://twitter.com/api/1/foobar')
68
69 assert resp.json() == {"error": "not found"}
70
71 assert len(responses.calls) == 1
72 assert responses.calls[0].request.url == 'http://twitter.com/api/1/foobar'
73 assert responses.calls[0].response.text == '{"error": "not found"}'
74
75 If you attempt to fetch a url which doesn't hit a match, ``responses`` will raise
76 a ``ConnectionError``:
77
78 .. code-block:: python
79
80 import responses
81 import requests
82
83 from requests.exceptions import ConnectionError
84
85 @responses.activate
86 def test_simple():
87 with pytest.raises(ConnectionError):
88 requests.get('http://twitter.com/api/1/foobar')
89
90 Lastly, you can pass an ``Exception`` as the body to trigger an error on the request:
91
92 .. code-block:: python
93
94 import responses
95 import requests
96
97 @responses.activate
98 def test_simple():
99 responses.add(responses.GET, 'http://twitter.com/api/1/foobar',
100 body=Exception('...'))
101 with pytest.raises(Exception):
102 requests.get('http://twitter.com/api/1/foobar')
103
104
105 Response Parameters
106 -------------------
107
108 Responses are automatically registered via params on ``add``, but can also be
109 passed directly:
110
111 .. code-block:: python
112
113 import responses
114
115 responses.add(
116 responses.Response(
117 method='GET',
118 url='http://example.com',
119 )
120 )
121
122 The following attributes can be passed to a Response mock:
123
124 method (``str``)
125 The HTTP method (GET, POST, etc).
126
127 url (``str`` or compiled regular expression)
128 The full resource URL.
129
130 match_querystring (``bool``)
131 Include the query string when matching requests.
132 Enabled by default if the response URL contains a query string,
133 disabled if it doesn't or the URL is a regular expression.
134
135 body (``str`` or ``BufferedReader``)
136 The response body.
137
138 json
139 A Python object representing the JSON response body. Automatically configures
140 the appropriate Content-Type.
141
142 status (``int``)
143 The HTTP status code.
144
145 content_type (``content_type``)
146 Defaults to ``text/plain``.
147
148 headers (``dict``)
149 Response headers.
150
151 stream (``bool``)
152 DEPRECATED: use ``stream`` argument in request directly
153
154 auto_calculate_content_length (``bool``)
155 Disabled by default. Automatically calculates the length of a supplied string or JSON body.
156
157 match (``list``)
158 A list of callbacks to match requests based on request attributes.
159 Current module provides multiple matchers that you can use to match:
160
161 * body contents in JSON format
162 * body contents in URL encoded data format
163 * request query parameters
164 * request query string (similar to query parameters but takes string as input)
165 * kwargs provided to request e.g. ``stream``, ``verify``
166 * 'multipart/form-data' content and headers in request
167 * request headers
168 * request fragment identifier
169
170 Alternatively user can create custom matcher.
171 Read more `Matching Requests`_
172
173
174 Matching Requests
175 -----------------
176
177 When adding responses for endpoints that are sent request data you can add
178 matchers to ensure your code is sending the right parameters and provide
179 different responses based on the request body contents. Responses provides
180 matchers for JSON and URL-encoded request bodies and you can supply your own for
181 other formats.
182
183 .. code-block:: python
184
185 import responses
186 import requests
187 from responses import matchers
188
189 @responses.activate
190 def test_calc_api():
191 responses.add(
192 responses.POST,
193 url='http://calc.com/sum',
194 body="4",
195 match=[
196 matchers.urlencoded_params_matcher({"left": "1", "right": "3"})
197 ]
198 )
199 requests.post("http://calc.com/sum", data={"left": 1, "right": 3})
200
201 Matching JSON encoded data can be done with ``matchers.json_params_matcher()``.
202 If your application uses other encodings you can build your own matcher that
203 returns ``True`` or ``False`` if the request parameters match. Your matcher can
204 expect a ``request`` parameter to be provided by responses.
205
206 Similarly, you can use the ``matchers.query_param_matcher`` function to match
207 against the ``params`` request parameter.
208 Note, you must set ``match_querystring=False``
209
210 .. code-block:: python
211
212 import responses
213 import requests
214 from responses import matchers
215
216 @responses.activate
217 def test_calc_api():
218 url = "http://example.com/test"
219 params = {"hello": "world", "I am": "a big test"}
220 responses.add(
221 method=responses.GET,
222 url=url,
223 body="test",
224 match=[matchers.query_param_matcher(params)],
225 match_querystring=False,
226 )
227
228 resp = requests.get(url, params=params)
229
230 constructed_url = r"http://example.com/test?I+am=a+big+test&hello=world"
231 assert resp.url == constructed_url
232 assert resp.request.url == constructed_url
233 assert resp.request.params == params
234
235
236 As alternative, you can use query string value in ``matchers.query_string_matcher``
237
238 .. code-block:: python
239
240 import requests
241 import responses
242 from responses import matchers
243
244 @responses.activate
245 def my_func():
246 responses.add(
247 responses.GET,
248 "https://httpbin.org/get",
249 match=[matchers.query_string_matcher("didi=pro&test=1")],
250 )
251 resp = requests.get("https://httpbin.org/get", params={"test": 1, "didi": "pro"})
252
253 my_func()
254
255 To validate request arguments use the ``matchers.request_kwargs_matcher`` function to match
256 against the request kwargs.
257 Note, only arguments provided to ``matchers.request_kwargs_matcher`` will be validated
258
259 .. code-block:: python
260
261 import responses
262 import requests
263 from responses import matchers
264
265 with responses.RequestsMock(assert_all_requests_are_fired=False) as rsps:
266 req_kwargs = {
267 "stream": True,
268 "verify": False,
269 }
270 rsps.add(
271 "GET",
272 "http://111.com",
273 match=[matchers.request_kwargs_matcher(req_kwargs)],
274 )
275
276 requests.get("http://111.com", stream=True)
277
278 # >>> Arguments don't match: {stream: True, verify: True} doesn't match {stream: True, verify: False}
279
280 To validate request body and headers for ``multipart/form-data`` data you can use
281 ``matchers.multipart_matcher``. The ``data``, and ``files`` parameters provided will be compared
282 to the request:
283
284 .. code-block:: python
285
286 import requests
287 import responses
288 from responses.matchers import multipart_matcher
289
290 @responses.activate
291 def my_func():
292 req_data = {"some": "other", "data": "fields"}
293 req_files = {"file_name": b"Old World!"}
294 responses.add(
295 responses.POST, url="http://httpbin.org/post",
296 match=[multipart_matcher(req_data, req_files)]
297 )
298 resp = requests.post("http://httpbin.org/post", files={"file_name": b"New World!"})
299
300 my_func()
301 # >>> raises ConnectionError: multipart/form-data doesn't match. Request body differs.
302
303
304 To validate request URL fragment identifier you can use ``matchers.fragment_identifier_matcher``.
305 The matcher takes fragment string (everything after ``#`` sign) as input for comparison:
306
307 .. code-block:: python
308
309 import requests
310 import responses
311 from responses.matchers import fragment_identifier_matcher
312
313 @responses.activate
314 def run():
315 url = "http://example.com?ab=xy&zed=qwe#test=1&foo=bar"
316 responses.add(
317 responses.GET,
318 url,
319 match_querystring=True,
320 match=[fragment_identifier_matcher("test=1&foo=bar")],
321 body=b"test",
322 )
323
324 # two requests to check reversed order of fragment identifier
325 resp = requests.get("http://example.com?ab=xy&zed=qwe#test=1&foo=bar")
326 resp = requests.get("http://example.com?zed=qwe&ab=xy#foo=bar&test=1")
327
328 run()
329
330 Matching Request Headers
331 ------------------------
332
333 When adding responses you can specify matchers to ensure that your code is
334 sending the right headers and provide different responses based on the request
335 headers.
336
337 .. code-block:: python
338
339 import responses
340 import requests
341 from responses import matchers
342
343
344 @responses.activate
345 def test_content_type():
346 responses.add(
347 responses.GET,
348 url="http://example.com/",
349 body="hello world",
350 match=[
351 matchers.header_matcher({"Accept": "text/plain"})
352 ]
353 )
354
355 responses.add(
356 responses.GET,
357 url="http://example.com/",
358 json={"content": "hello world"},
359 match=[
360 matchers.header_matcher({"Accept": "application/json"})
361 ]
362 )
363
364 # request in reverse order to how they were added!
365 resp = requests.get("http://example.com/", headers={"Accept": "application/json"})
366 assert resp.json() == {"content": "hello world"}
367
368 resp = requests.get("http://example.com/", headers={"Accept": "text/plain"})
369 assert resp.text == "hello world"
370
371 Because ``requests`` will send several standard headers in addition to what was
372 specified by your code, request headers that are additional to the ones
373 passed to the matcher are ignored by default. You can change this behaviour by
374 passing ``strict_match=True`` to the matcher to ensure that only the headers
375 that you're expecting are sent and no others. Note that you will probably have
376 to use a ``PreparedRequest`` in your code to ensure that ``requests`` doesn't
377 include any additional headers.
378
379 .. code-block:: python
380
381 import responses
382 import requests
383 from responses import matchers
384
385 @responses.activate
386 def test_content_type():
387 responses.add(
388 responses.GET,
389 url="http://example.com/",
390 body="hello world",
391 match=[
392 matchers.header_matcher({"Accept": "text/plain"}, strict_match=True)
393 ]
394 )
395
396 # this will fail because requests adds its own headers
397 with pytest.raises(ConnectionError):
398 requests.get("http://example.com/", headers={"Accept": "text/plain"})
399
400 # a prepared request where you overwrite the headers before sending will work
401 session = requests.Session()
402 prepped = session.prepare_request(
403 requests.Request(
404 method="GET",
405 url="http://example.com/",
406 )
407 )
408 prepped.headers = {"Accept": "text/plain"}
409
410 resp = session.send(prepped)
411 assert resp.text == "hello world"
412
413 Dynamic Responses
414 -----------------
415
416 You can utilize callbacks to provide dynamic responses. The callback must return
417 a tuple of (``status``, ``headers``, ``body``).
418
419 .. code-block:: python
420
421 import json
422
423 import responses
424 import requests
425
426 @responses.activate
427 def test_calc_api():
428
429 def request_callback(request):
430 payload = json.loads(request.body)
431 resp_body = {'value': sum(payload['numbers'])}
432 headers = {'request-id': '728d329e-0e86-11e4-a748-0c84dc037c13'}
433 return (200, headers, json.dumps(resp_body))
434
435 responses.add_callback(
436 responses.POST, 'http://calc.com/sum',
437 callback=request_callback,
438 content_type='application/json',
439 )
440
441 resp = requests.post(
442 'http://calc.com/sum',
443 json.dumps({'numbers': [1, 2, 3]}),
444 headers={'content-type': 'application/json'},
445 )
446
447 assert resp.json() == {'value': 6}
448
449 assert len(responses.calls) == 1
450 assert responses.calls[0].request.url == 'http://calc.com/sum'
451 assert responses.calls[0].response.text == '{"value": 6}'
452 assert (
453 responses.calls[0].response.headers['request-id'] ==
454 '728d329e-0e86-11e4-a748-0c84dc037c13'
455 )
456
457 You can also pass a compiled regex to ``add_callback`` to match multiple urls:
458
459 .. code-block:: python
460
461 import re, json
462
463 from functools import reduce
464
465 import responses
466 import requests
467
468 operators = {
469 'sum': lambda x, y: x+y,
470 'prod': lambda x, y: x*y,
471 'pow': lambda x, y: x**y
472 }
473
474 @responses.activate
475 def test_regex_url():
476
477 def request_callback(request):
478 payload = json.loads(request.body)
479 operator_name = request.path_url[1:]
480
481 operator = operators[operator_name]
482
483 resp_body = {'value': reduce(operator, payload['numbers'])}
484 headers = {'request-id': '728d329e-0e86-11e4-a748-0c84dc037c13'}
485 return (200, headers, json.dumps(resp_body))
486
487 responses.add_callback(
488 responses.POST,
489 re.compile('http://calc.com/(sum|prod|pow|unsupported)'),
490 callback=request_callback,
491 content_type='application/json',
492 )
493
494 resp = requests.post(
495 'http://calc.com/prod',
496 json.dumps({'numbers': [2, 3, 4]}),
497 headers={'content-type': 'application/json'},
498 )
499 assert resp.json() == {'value': 24}
500
501 test_regex_url()
502
503
504 If you want to pass extra keyword arguments to the callback function, for example when reusing
505 a callback function to give a slightly different result, you can use ``functools.partial``:
506
507 .. code-block:: python
508
509 from functools import partial
510
511 ...
512
513 def request_callback(request, id=None):
514 payload = json.loads(request.body)
515 resp_body = {'value': sum(payload['numbers'])}
516 headers = {'request-id': id}
517 return (200, headers, json.dumps(resp_body))
518
519 responses.add_callback(
520 responses.POST, 'http://calc.com/sum',
521 callback=partial(request_callback, id='728d329e-0e86-11e4-a748-0c84dc037c13'),
522 content_type='application/json',
523 )
524
525
526 You can see params passed in the original ``request`` in ``responses.calls[].request.params``:
527
528 .. code-block:: python
529
530 import responses
531 import requests
532
533 @responses.activate
534 def test_request_params():
535 responses.add(
536 method=responses.GET,
537 url="http://example.com?hello=world",
538 body="test",
539 match_querystring=False,
540 )
541
542 resp = requests.get('http://example.com', params={"hello": "world"})
543 assert responses.calls[0].request.params == {"hello": "world"}
544
545 Responses as a context manager
546 ------------------------------
547
548 .. code-block:: python
549
550 import responses
551 import requests
552
553 def test_my_api():
554 with responses.RequestsMock() as rsps:
555 rsps.add(responses.GET, 'http://twitter.com/api/1/foobar',
556 body='{}', status=200,
557 content_type='application/json')
558 resp = requests.get('http://twitter.com/api/1/foobar')
559
560 assert resp.status_code == 200
561
562 # outside the context manager requests will hit the remote server
563 resp = requests.get('http://twitter.com/api/1/foobar')
564 resp.status_code == 404
565
566 Responses as a pytest fixture
567 -----------------------------
568
569 .. code-block:: python
570
571 @pytest.fixture
572 def mocked_responses():
573 with responses.RequestsMock() as rsps:
574 yield rsps
575
576 def test_api(mocked_responses):
577 mocked_responses.add(
578 responses.GET, 'http://twitter.com/api/1/foobar',
579 body='{}', status=200,
580 content_type='application/json')
581 resp = requests.get('http://twitter.com/api/1/foobar')
582 assert resp.status_code == 200
583
584 Responses inside a unittest setUp()
585 -----------------------------------
586
587 When run with unittest tests, this can be used to set up some
588 generic class-level responses, that may be complemented by each test
589
590 .. code-block:: python
591
592 def setUp():
593 self.responses = responses.RequestsMock()
594 self.responses.start()
595
596 # self.responses.add(...)
597
598 self.addCleanup(self.responses.stop)
599 self.addCleanup(self.responses.reset)
600
601 def test_api(self):
602 self.responses.add(
603 responses.GET, 'http://twitter.com/api/1/foobar',
604 body='{}', status=200,
605 content_type='application/json')
606 resp = requests.get('http://twitter.com/api/1/foobar')
607 assert resp.status_code == 200
608
609 Assertions on declared responses
610 --------------------------------
611
612 When used as a context manager, Responses will, by default, raise an assertion
613 error if a url was registered but not accessed. This can be disabled by passing
614 the ``assert_all_requests_are_fired`` value:
615
616 .. code-block:: python
617
618 import responses
619 import requests
620
621 def test_my_api():
622 with responses.RequestsMock(assert_all_requests_are_fired=False) as rsps:
623 rsps.add(responses.GET, 'http://twitter.com/api/1/foobar',
624 body='{}', status=200,
625 content_type='application/json')
626
627 assert_call_count
628 -----------------
629
630 Assert that the request was called exactly n times.
631
632 .. code-block:: python
633
634 import responses
635 import requests
636
637 @responses.activate
638 def test_assert_call_count():
639 responses.add(responses.GET, "http://example.com")
640
641 requests.get("http://example.com")
642 assert responses.assert_call_count("http://example.com", 1) is True
643
644 requests.get("http://example.com")
645 with pytest.raises(AssertionError) as excinfo:
646 responses.assert_call_count("http://example.com", 1)
647 assert "Expected URL 'http://example.com' to be called 1 times. Called 2 times." in str(excinfo.value)
648
649
650 Multiple Responses
651 ------------------
652
653 You can also add multiple responses for the same url:
654
655 .. code-block:: python
656
657 import responses
658 import requests
659
660 @responses.activate
661 def test_my_api():
662 responses.add(responses.GET, 'http://twitter.com/api/1/foobar', status=500)
663 responses.add(responses.GET, 'http://twitter.com/api/1/foobar',
664 body='{}', status=200,
665 content_type='application/json')
666
667 resp = requests.get('http://twitter.com/api/1/foobar')
668 assert resp.status_code == 500
669 resp = requests.get('http://twitter.com/api/1/foobar')
670 assert resp.status_code == 200
671
672
673 Using a callback to modify the response
674 ---------------------------------------
675
676 If you use customized processing in `requests` via subclassing/mixins, or if you
677 have library tools that interact with `requests` at a low level, you may need
678 to add extended processing to the mocked Response object to fully simulate the
679 environment for your tests. A `response_callback` can be used, which will be
680 wrapped by the library before being returned to the caller. The callback
681 accepts a `response` as it's single argument, and is expected to return a
682 single `response` object.
683
684 .. code-block:: python
685
686 import responses
687 import requests
688
689 def response_callback(resp):
690 resp.callback_processed = True
691 return resp
692
693 with responses.RequestsMock(response_callback=response_callback) as m:
694 m.add(responses.GET, 'http://example.com', body=b'test')
695 resp = requests.get('http://example.com')
696 assert resp.text == "test"
697 assert hasattr(resp, 'callback_processed')
698 assert resp.callback_processed is True
699
700
701 Passing through real requests
702 -----------------------------
703
704 In some cases you may wish to allow for certain requests to pass through responses
705 and hit a real server. This can be done with the ``add_passthru`` methods:
706
707 .. code-block:: python
708
709 import responses
710
711 @responses.activate
712 def test_my_api():
713 responses.add_passthru('https://percy.io')
714
715 This will allow any requests matching that prefix, that is otherwise not
716 registered as a mock response, to passthru using the standard behavior.
717
718 Pass through endpoints can be configured with regex patterns if you
719 need to allow an entire domain or path subtree to send requests:
720
721 .. code-block:: python
722
723 responses.add_passthru(re.compile('https://percy.io/\\w+'))
724
725
726 Lastly, you can use the `response.passthrough` attribute on `BaseResponse` or
727 use ``PassthroughResponse`` to enable a response to behave as a pass through.
728
729 .. code-block:: python
730
731 # Enable passthrough for a single response
732 response = Response(responses.GET, 'http://example.com', body='not used')
733 response.passthrough = True
734 responses.add(response)
735
736 # Use PassthroughResponse
737 response = PassthroughResponse(responses.GET, 'http://example.com')
738 responses.add(response)
739
740 Viewing/Modifying registered responses
741 --------------------------------------
742
743 Registered responses are available as a public method of the RequestMock
744 instance. It is sometimes useful for debugging purposes to view the stack of
745 registered responses which can be accessed via ``responses.registered()``.
746
747 The ``replace`` function allows a previously registered ``response`` to be
748 changed. The method signature is identical to ``add``. ``response`` s are
749 identified using ``method`` and ``url``. Only the first matched ``response`` is
750 replaced.
751
752 .. code-block:: python
753
754 import responses
755 import requests
756
757 @responses.activate
758 def test_replace():
759
760 responses.add(responses.GET, 'http://example.org', json={'data': 1})
761 responses.replace(responses.GET, 'http://example.org', json={'data': 2})
762
763 resp = requests.get('http://example.org')
764
765 assert resp.json() == {'data': 2}
766
767
768 The ``upsert`` function allows a previously registered ``response`` to be
769 changed like ``replace``. If the response is registered, the ``upsert`` function
770 will registered it like ``add``.
771
772 ``remove`` takes a ``method`` and ``url`` argument and will remove **all**
773 matched responses from the registered list.
774
775 Finally, ``reset`` will reset all registered responses.
776
777 Contributing
778 ------------
779
780 Responses uses several linting and autoformatting utilities, so it's important that when
781 submitting patches you use the appropriate toolchain:
782
783 Clone the repository:
784
785 .. code-block:: shell
786
787 git clone https://github.com/getsentry/responses.git
788
789 Create an environment (e.g. with ``virtualenv``):
790
791 .. code-block:: shell
792
793 virtualenv .env && source .env/bin/activate
794
795 Configure development requirements:
796
797 .. code-block:: shell
798
799 make develop
800
801 Responses uses `Pytest <https://docs.pytest.org/en/latest/>`_ for
802 testing. You can run all tests by:
803
804 .. code-block:: shell
805
806 pytest
807
808 And run a single test by:
809
810 .. code-block:: shell
811
812 pytest -k '<test_function_name>'
813
814 To verify ``type`` compliance, run `mypy <https://github.com/python/mypy>`_ linter:
815
816 .. code-block:: shell
817
818 mypy --config-file=./mypy.ini -p responses
819
820 To check code style and reformat it run:
821
822 .. code-block:: shell
823
824 pre-commit run --all-files
825
826 Note: on some OS, you have to use ``pre_commit``
827
828
55 setup.py
66 tox.ini
77 responses/__init__.py
8 responses/__init__.pyi
9 responses/matchers.py
10 responses/matchers.pyi
11 responses/test_matchers.py
812 responses/test_responses.py
913 responses.egg-info/PKG-INFO
1014 responses.egg-info/SOURCES.txt
00 requests>=2.0
1 six
12 urllib3>=1.25.10
2 six
33
44 [:python_version < "3.3"]
55 mock
99
1010 [tests]
1111 coverage<6.0.0,>=3.7.1
12 flake8
1213 pytest-cov
1314 pytest-localserver
14 flake8
1515 types-mock
1616 types-requests
1717 types-six
2020 pytest<5.0,>=4.6
2121
2222 [tests:python_version >= "3.5"]
23 mypy
2324 pytest>=4.6
24 mypy
5858
5959 setup(
6060 name="responses",
61 version="0.13.4",
61 version="0.14.0",
6262 author="David Cramer",
6363 description=("A utility library for mocking out the `requests` Python library."),
6464 url="https://github.com/getsentry/responses",
8888 "Programming Language :: Python :: 3.7",
8989 "Programming Language :: Python :: 3.8",
9090 "Programming Language :: Python :: 3.9",
91 "Programming Language :: Python :: 3.10",
9192 "Topic :: Software Development",
9293 ],
9394 )
00 [tox]
1 envlist = py27,py35,py36,py37,py38,py39
1 envlist = py27,py35,py36,py37,py38,py39,py310,mypy
22
33 [testenv]
44 extras = tests
55 commands =
66 pytest . --cov responses --cov-report term-missing
7
8
9 [testenv:mypy]
10 description = Check types using 'mypy'
11 basepython = python3.7
12 commands =
13 python -m mypy --config-file=mypy.ini -p responses