454 lines
13 KiB
Text
454 lines
13 KiB
Text
Metadata-Version: 2.1
|
|
Name: responses
|
|
Version: 0.10.6
|
|
Summary: A utility library for mocking out the `requests` Python library.
|
|
Home-page: https://github.com/getsentry/responses
|
|
Author: David Cramer
|
|
License: Apache 2.0
|
|
Platform: UNKNOWN
|
|
Classifier: Intended Audience :: Developers
|
|
Classifier: Intended Audience :: System Administrators
|
|
Classifier: Operating System :: OS Independent
|
|
Classifier: Programming Language :: Python
|
|
Classifier: Programming Language :: Python :: 2
|
|
Classifier: Programming Language :: Python :: 2.7
|
|
Classifier: Programming Language :: Python :: 3
|
|
Classifier: Programming Language :: Python :: 3.4
|
|
Classifier: Programming Language :: Python :: 3.5
|
|
Classifier: Programming Language :: Python :: 3.6
|
|
Classifier: Programming Language :: Python :: 3.7
|
|
Classifier: Topic :: Software Development
|
|
Requires-Python: >=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*
|
|
Requires-Dist: requests (>=2.0)
|
|
Requires-Dist: six
|
|
Requires-Dist: mock ; python_version < "3.3"
|
|
Requires-Dist: cookies ; python_version < "3.4"
|
|
Provides-Extra: tests
|
|
Requires-Dist: pytest ; extra == 'tests'
|
|
Requires-Dist: coverage (<5.0.0,>=3.7.1) ; extra == 'tests'
|
|
Requires-Dist: pytest-cov ; extra == 'tests'
|
|
Requires-Dist: pytest-localserver ; extra == 'tests'
|
|
Requires-Dist: flake8 ; extra == 'tests'
|
|
|
|
Responses
|
|
=========
|
|
|
|
.. image:: https://travis-ci.org/getsentry/responses.svg?branch=master
|
|
:target: https://travis-ci.org/getsentry/responses
|
|
|
|
A utility library for mocking out the `requests` Python library.
|
|
|
|
.. note::
|
|
|
|
Responses requires Python 2.7 or newer, and requests >= 2.0
|
|
|
|
|
|
Installing
|
|
----------
|
|
|
|
``pip install responses``
|
|
|
|
|
|
Basics
|
|
------
|
|
|
|
The core of ``responses`` comes from registering mock responses:
|
|
|
|
.. code-block:: python
|
|
|
|
import responses
|
|
import requests
|
|
|
|
@responses.activate
|
|
def test_simple():
|
|
responses.add(responses.GET, 'http://twitter.com/api/1/foobar',
|
|
json={'error': 'not found'}, status=404)
|
|
|
|
resp = requests.get('http://twitter.com/api/1/foobar')
|
|
|
|
assert resp.json() == {"error": "not found"}
|
|
|
|
assert len(responses.calls) == 1
|
|
assert responses.calls[0].request.url == 'http://twitter.com/api/1/foobar'
|
|
assert responses.calls[0].response.text == '{"error": "not found"}'
|
|
|
|
If you attempt to fetch a url which doesn't hit a match, ``responses`` will raise
|
|
a ``ConnectionError``:
|
|
|
|
.. code-block:: python
|
|
|
|
import responses
|
|
import requests
|
|
|
|
from requests.exceptions import ConnectionError
|
|
|
|
@responses.activate
|
|
def test_simple():
|
|
with pytest.raises(ConnectionError):
|
|
requests.get('http://twitter.com/api/1/foobar')
|
|
|
|
Lastly, you can pass an ``Exception`` as the body to trigger an error on the request:
|
|
|
|
.. code-block:: python
|
|
|
|
import responses
|
|
import requests
|
|
|
|
@responses.activate
|
|
def test_simple():
|
|
responses.add(responses.GET, 'http://twitter.com/api/1/foobar',
|
|
body=Exception('...'))
|
|
with pytest.raises(Exception):
|
|
requests.get('http://twitter.com/api/1/foobar')
|
|
|
|
|
|
Response Parameters
|
|
-------------------
|
|
|
|
Responses are automatically registered via params on ``add``, but can also be
|
|
passed directly:
|
|
|
|
.. code-block:: python
|
|
|
|
import responses
|
|
|
|
responses.add(
|
|
responses.Response(
|
|
method='GET',
|
|
url='http://example.com',
|
|
)
|
|
)
|
|
|
|
The following attributes can be passed to a Response mock:
|
|
|
|
method (``str``)
|
|
The HTTP method (GET, POST, etc).
|
|
|
|
url (``str`` or compiled regular expression)
|
|
The full resource URL.
|
|
|
|
match_querystring (``bool``)
|
|
Include the query string when matching requests.
|
|
Enabled by default if the response URL contains a query string,
|
|
disabled if it doesn't or the URL is a regular expression.
|
|
|
|
body (``str`` or ``BufferedReader``)
|
|
The response body.
|
|
|
|
json
|
|
A Python object representing the JSON response body. Automatically configures
|
|
the appropriate Content-Type.
|
|
|
|
status (``int``)
|
|
The HTTP status code.
|
|
|
|
content_type (``content_type``)
|
|
Defaults to ``text/plain``.
|
|
|
|
headers (``dict``)
|
|
Response headers.
|
|
|
|
stream (``bool``)
|
|
Disabled by default. Indicates the response should use the streaming API.
|
|
|
|
|
|
Dynamic Responses
|
|
-----------------
|
|
|
|
You can utilize callbacks to provide dynamic responses. The callback must return
|
|
a tuple of (``status``, ``headers``, ``body``).
|
|
|
|
.. code-block:: python
|
|
|
|
import json
|
|
|
|
import responses
|
|
import requests
|
|
|
|
@responses.activate
|
|
def test_calc_api():
|
|
|
|
def request_callback(request):
|
|
payload = json.loads(request.body)
|
|
resp_body = {'value': sum(payload['numbers'])}
|
|
headers = {'request-id': '728d329e-0e86-11e4-a748-0c84dc037c13'}
|
|
return (200, headers, json.dumps(resp_body))
|
|
|
|
responses.add_callback(
|
|
responses.POST, 'http://calc.com/sum',
|
|
callback=request_callback,
|
|
content_type='application/json',
|
|
)
|
|
|
|
resp = requests.post(
|
|
'http://calc.com/sum',
|
|
json.dumps({'numbers': [1, 2, 3]}),
|
|
headers={'content-type': 'application/json'},
|
|
)
|
|
|
|
assert resp.json() == {'value': 6}
|
|
|
|
assert len(responses.calls) == 1
|
|
assert responses.calls[0].request.url == 'http://calc.com/sum'
|
|
assert responses.calls[0].response.text == '{"value": 6}'
|
|
assert (
|
|
responses.calls[0].response.headers['request-id'] ==
|
|
'728d329e-0e86-11e4-a748-0c84dc037c13'
|
|
)
|
|
|
|
You can also pass a compiled regex to `add_callback` to match multiple urls:
|
|
|
|
.. code-block:: python
|
|
|
|
import re, json
|
|
|
|
from functools import reduce
|
|
|
|
import responses
|
|
import requests
|
|
|
|
operators = {
|
|
'sum': lambda x, y: x+y,
|
|
'prod': lambda x, y: x*y,
|
|
'pow': lambda x, y: x**y
|
|
}
|
|
|
|
@responses.activate
|
|
def test_regex_url():
|
|
|
|
def request_callback(request):
|
|
payload = json.loads(request.body)
|
|
operator_name = request.path_url[1:]
|
|
|
|
operator = operators[operator_name]
|
|
|
|
resp_body = {'value': reduce(operator, payload['numbers'])}
|
|
headers = {'request-id': '728d329e-0e86-11e4-a748-0c84dc037c13'}
|
|
return (200, headers, json.dumps(resp_body))
|
|
|
|
responses.add_callback(
|
|
responses.POST,
|
|
re.compile('http://calc.com/(sum|prod|pow|unsupported)'),
|
|
callback=request_callback,
|
|
content_type='application/json',
|
|
)
|
|
|
|
resp = requests.post(
|
|
'http://calc.com/prod',
|
|
json.dumps({'numbers': [2, 3, 4]}),
|
|
headers={'content-type': 'application/json'},
|
|
)
|
|
assert resp.json() == {'value': 24}
|
|
|
|
test_regex_url()
|
|
|
|
|
|
If you want to pass extra keyword arguments to the callback function, for example when reusing
|
|
a callback function to give a slightly different result, you can use ``functools.partial``:
|
|
|
|
.. code-block:: python
|
|
|
|
from functools import partial
|
|
|
|
...
|
|
|
|
def request_callback(request, id=None):
|
|
payload = json.loads(request.body)
|
|
resp_body = {'value': sum(payload['numbers'])}
|
|
headers = {'request-id': id}
|
|
return (200, headers, json.dumps(resp_body))
|
|
|
|
responses.add_callback(
|
|
responses.POST, 'http://calc.com/sum',
|
|
callback=partial(request_callback, id='728d329e-0e86-11e4-a748-0c84dc037c13'),
|
|
content_type='application/json',
|
|
)
|
|
|
|
|
|
Responses as a context manager
|
|
------------------------------
|
|
|
|
.. code-block:: python
|
|
|
|
import responses
|
|
import requests
|
|
|
|
def test_my_api():
|
|
with responses.RequestsMock() as rsps:
|
|
rsps.add(responses.GET, 'http://twitter.com/api/1/foobar',
|
|
body='{}', status=200,
|
|
content_type='application/json')
|
|
resp = requests.get('http://twitter.com/api/1/foobar')
|
|
|
|
assert resp.status_code == 200
|
|
|
|
# outside the context manager requests will hit the remote server
|
|
resp = requests.get('http://twitter.com/api/1/foobar')
|
|
resp.status_code == 404
|
|
|
|
Responses as a pytest fixture
|
|
-----------------------------
|
|
|
|
.. code-block:: python
|
|
|
|
@pytest.fixture
|
|
def mocked_responses():
|
|
with responses.RequestsMock() as rsps:
|
|
yield rsps
|
|
|
|
def test_api(mocked_responses):
|
|
mocked_responses.add(
|
|
responses.GET, 'http://twitter.com/api/1/foobar',
|
|
body='{}', status=200,
|
|
content_type='application/json')
|
|
resp = requests.get('http://twitter.com/api/1/foobar')
|
|
assert resp.status_code == 200
|
|
|
|
Assertions on declared responses
|
|
--------------------------------
|
|
|
|
When used as a context manager, Responses will, by default, raise an assertion
|
|
error if a url was registered but not accessed. This can be disabled by passing
|
|
the ``assert_all_requests_are_fired`` value:
|
|
|
|
.. code-block:: python
|
|
|
|
import responses
|
|
import requests
|
|
|
|
def test_my_api():
|
|
with responses.RequestsMock(assert_all_requests_are_fired=False) as rsps:
|
|
rsps.add(responses.GET, 'http://twitter.com/api/1/foobar',
|
|
body='{}', status=200,
|
|
content_type='application/json')
|
|
|
|
|
|
Multiple Responses
|
|
------------------
|
|
|
|
You can also add multiple responses for the same url:
|
|
|
|
.. code-block:: python
|
|
|
|
import responses
|
|
import requests
|
|
|
|
@responses.activate
|
|
def test_my_api():
|
|
responses.add(responses.GET, 'http://twitter.com/api/1/foobar', status=500)
|
|
responses.add(responses.GET, 'http://twitter.com/api/1/foobar',
|
|
body='{}', status=200,
|
|
content_type='application/json')
|
|
|
|
resp = requests.get('http://twitter.com/api/1/foobar')
|
|
assert resp.status_code == 500
|
|
resp = requests.get('http://twitter.com/api/1/foobar')
|
|
assert resp.status_code == 200
|
|
|
|
|
|
Using a callback to modify the response
|
|
---------------------------------------
|
|
|
|
If you use customized processing in `requests` via subclassing/mixins, or if you
|
|
have library tools that interact with `requests` at a low level, you may need
|
|
to add extended processing to the mocked Response object to fully simulate the
|
|
environment for your tests. A `response_callback` can be used, which will be
|
|
wrapped by the library before being returned to the caller. The callback
|
|
accepts a `response` as it's single argument, and is expected to return a
|
|
single `response` object.
|
|
|
|
.. code-block:: python
|
|
|
|
import responses
|
|
import requests
|
|
|
|
def response_callback(resp):
|
|
resp.callback_processed = True
|
|
return resp
|
|
|
|
with responses.RequestsMock(response_callback=response_callback) as m:
|
|
m.add(responses.GET, 'http://example.com', body=b'test')
|
|
resp = requests.get('http://example.com')
|
|
assert resp.text == "test"
|
|
assert hasattr(resp, 'callback_processed')
|
|
assert resp.callback_processed is True
|
|
|
|
|
|
Passing thru real requests
|
|
--------------------------
|
|
|
|
In some cases you may wish to allow for certain requests to pass thru responses
|
|
and hit a real server. This can be done with the 'passthru' methods:
|
|
|
|
.. code-block:: python
|
|
|
|
import responses
|
|
|
|
@responses.activate
|
|
def test_my_api():
|
|
responses.add_passthru('https://percy.io')
|
|
|
|
This will allow any requests matching that prefix, that is otherwise not registered
|
|
as a mock response, to passthru using the standard behavior.
|
|
|
|
|
|
Viewing/Modifying registered responses
|
|
--------------------------------------
|
|
|
|
Registered responses are available as a private attribute of the RequestMock
|
|
instance. It is sometimes useful for debugging purposes to view the stack of
|
|
registered responses which can be accessed via ``responses.mock._matches``.
|
|
|
|
The ``replace`` function allows a previously registered ``response`` to be
|
|
changed. The method signature is identical to ``add``. ``response``s are
|
|
identified using ``method`` and ``url``. Only the first matched ``response`` is
|
|
replaced.
|
|
|
|
.. code-block:: python
|
|
|
|
import responses
|
|
import requests
|
|
|
|
@responses.activate
|
|
def test_replace():
|
|
|
|
responses.add(responses.GET, 'http://example.org', json={'data': 1})
|
|
responses.replace(responses.GET, 'http://example.org', json={'data': 2})
|
|
|
|
resp = requests.get('http://example.org')
|
|
|
|
assert resp.json() == {'data': 2}
|
|
|
|
|
|
``remove`` takes a ``method`` and ``url`` argument and will remove *all*
|
|
matched ``response``s from the registered list.
|
|
|
|
Finally, ``clear`` will reset all registered ``response``s
|
|
|
|
|
|
|
|
Contributing
|
|
------------
|
|
|
|
Responses uses several linting and autoformatting utilities, so it's important that when
|
|
submitting patches you use the appropriate toolchain:
|
|
|
|
Clone the repository:
|
|
|
|
.. code-block:: shell
|
|
|
|
git clone https://github.com/getsentry/responses.git
|
|
|
|
Create an environment (e.g. with ``virtualenv``):
|
|
|
|
.. code-block:: shell
|
|
|
|
virtualenv .env && source .env/bin/activate
|
|
|
|
Configure development requirements:
|
|
|
|
.. code-block:: shell
|
|
|
|
make develop
|
|
|
|
|