summaryrefslogtreecommitdiffstats
path: root/third_party/python/responses
diff options
context:
space:
mode:
Diffstat (limited to 'third_party/python/responses')
-rw-r--r--third_party/python/responses/CHANGES119
-rw-r--r--third_party/python/responses/LICENSE201
-rw-r--r--third_party/python/responses/MANIFEST.in3
-rw-r--r--third_party/python/responses/PKG-INFO443
-rw-r--r--third_party/python/responses/README.rst420
-rw-r--r--third_party/python/responses/responses.py653
-rw-r--r--third_party/python/responses/setup.cfg16
-rw-r--r--third_party/python/responses/setup.py85
-rw-r--r--third_party/python/responses/test_responses.py924
-rw-r--r--third_party/python/responses/tox.ini7
10 files changed, 2871 insertions, 0 deletions
diff --git a/third_party/python/responses/CHANGES b/third_party/python/responses/CHANGES
new file mode 100644
index 0000000000..569f52f99d
--- /dev/null
+++ b/third_party/python/responses/CHANGES
@@ -0,0 +1,119 @@
+0.10.6
+------
+
+- Improved documentation.
+- Improved installation requirements for py3
+- ConnectionError's raised by responses now indicate which request
+ path/method failed to match a mock.
+- `test_responses.py` is no longer part of the installation targets.
+
+0.10.5
+------
+
+- Improved support for raising exceptions from callback mocks. If a mock
+ callback returns an exception object that exception will be raised.
+
+0.10.4
+------
+
+- Fixed generated wrapper when using `@responses.activate` in Python 3.6+
+ when decorated functions use parameter and/or return annotations.
+
+0.10.3
+------
+
+- Fixed deprecation warnings in python 3.7 for inspect module usage.
+
+0.10.2
+------
+
+- Fixed build setup to use undeprecated `pytest` bin stub.
+- Updated `tox` configuration.
+- Added example of using responses with `pytest.fixture`
+- Removed dependency on `biscuits` in py3. Instead `http.cookies` is being used.
+
+0.10.1
+------
+
+- Packaging fix to distribute wheel (#219)
+
+0.10.0
+------
+
+- Fix passing through extra settings (#207)
+- Fix collections.abc warning on Python 3.7 (#215)
+- Use 'biscuits' library instead of 'cookies' on Python 3.4+ (#218)
+
+0.9.0
+-----
+
+- Support for Python 3.7 (#196)
+- Support streaming responses for BaseResponse (#192)
+- Support custom patch targets for mock (#189)
+- Fix unicode support for passthru urls (#178)
+- Fix support for unicode in domain names and tlds (177)
+
+0.8.0
+-----
+
+- Added the ability to passthru real requests via ``add_passthru()``
+ and ``passthru_prefixes`` configurations.
+
+0.7.0
+-----
+
+- Responses will now be rotated until the final match is hit, and
+ then persist using that response (GH-171).
+
+0.6.2
+-----
+
+- Fixed call counting with exceptions (GH-163).
+- Fixed behavior with arbitrary status codes (GH-164).
+- Fixed handling of multiple responses with the same match (GH-165).
+- Fixed default path behavior with ``match_querystring`` (GH-166).
+
+0.6.1
+-----
+
+- Restored ``adding_headers`` compatibility (GH-160).
+
+0.6.0
+-----
+
+- Allow empty list/dict as json object (GH-100).
+- Added `response_callback` (GH-151).
+- Added ``Response`` interfaces (GH-155).
+- Fixed unicode characters in querystring (GH-153).
+- Added support for streaming IO buffers (GH-154).
+- Added support for empty (unset) Content-Type (GH-139).
+- Added reason to mocked responses (GH-132).
+- ``yapf`` autoformatting now enforced on codebase.
+
+0.5.1
+-----
+
+- Add LICENSE, README and CHANGES to the PyPI distribution (GH-97).
+
+0.5.0
+-----
+
+- Allow passing a JSON body to `response.add` (GH-82)
+- Improve ConnectionError emulation (GH-73)
+- Correct assertion in assert_all_requests_are_fired (GH-71)
+
+0.4.0
+-----
+
+- Requests 2.0+ is required
+- Mocking now happens on the adapter instead of the session
+
+0.3.0
+-----
+
+- Add the ability to mock errors (GH-22)
+- Add responses.mock context manager (GH-36)
+- Support custom adapters (GH-33)
+- Add support for regexp error matching (GH-25)
+- Add support for dynamic bodies via `responses.add_callback` (GH-24)
+- Preserve argspec when using `responses.activate` decorator (GH-18)
diff --git a/third_party/python/responses/LICENSE b/third_party/python/responses/LICENSE
new file mode 100644
index 0000000000..52b44b20a3
--- /dev/null
+++ b/third_party/python/responses/LICENSE
@@ -0,0 +1,201 @@
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+END OF TERMS AND CONDITIONS
+
+APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+Copyright 2015 David Cramer
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
diff --git a/third_party/python/responses/MANIFEST.in b/third_party/python/responses/MANIFEST.in
new file mode 100644
index 0000000000..12977a99c8
--- /dev/null
+++ b/third_party/python/responses/MANIFEST.in
@@ -0,0 +1,3 @@
+include README.rst CHANGES LICENSE
+include test_responses.py tox.ini
+global-exclude *~
diff --git a/third_party/python/responses/PKG-INFO b/third_party/python/responses/PKG-INFO
new file mode 100644
index 0000000000..54677a9dd3
--- /dev/null
+++ b/third_party/python/responses/PKG-INFO
@@ -0,0 +1,443 @@
+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
+Description: 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
+
+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.*
+Provides-Extra: tests
diff --git a/third_party/python/responses/README.rst b/third_party/python/responses/README.rst
new file mode 100644
index 0000000000..73a6d7a8f2
--- /dev/null
+++ b/third_party/python/responses/README.rst
@@ -0,0 +1,420 @@
+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
diff --git a/third_party/python/responses/responses.py b/third_party/python/responses/responses.py
new file mode 100644
index 0000000000..9de936805c
--- /dev/null
+++ b/third_party/python/responses/responses.py
@@ -0,0 +1,653 @@
+from __future__ import absolute_import, print_function, division, unicode_literals
+
+import _io
+import inspect
+import json as json_module
+import logging
+import re
+import six
+
+from collections import namedtuple
+from functools import update_wrapper
+from requests.adapters import HTTPAdapter
+from requests.exceptions import ConnectionError
+from requests.sessions import REDIRECT_STATI
+from requests.utils import cookiejar_from_dict
+
+try:
+ from collections.abc import Sequence, Sized
+except ImportError:
+ from collections import Sequence, Sized
+
+try:
+ from requests.packages.urllib3.response import HTTPResponse
+except ImportError:
+ from urllib3.response import HTTPResponse
+
+if six.PY2:
+ from urlparse import urlparse, parse_qsl, urlsplit, urlunsplit
+ from urllib import quote
+else:
+ from urllib.parse import urlparse, parse_qsl, urlsplit, urlunsplit, quote
+
+if six.PY2:
+ try:
+ from six import cStringIO as BufferIO
+ except ImportError:
+ from six import StringIO as BufferIO
+else:
+ from io import BytesIO as BufferIO
+
+try:
+ from unittest import mock as std_mock
+except ImportError:
+ import mock as std_mock
+
+try:
+ Pattern = re._pattern_type
+except AttributeError:
+ # Python 3.7
+ Pattern = re.Pattern
+
+UNSET = object()
+
+Call = namedtuple("Call", ["request", "response"])
+
+_real_send = HTTPAdapter.send
+
+logger = logging.getLogger("responses")
+
+
+def _is_string(s):
+ return isinstance(s, six.string_types)
+
+
+def _has_unicode(s):
+ return any(ord(char) > 128 for char in s)
+
+
+def _clean_unicode(url):
+ # Clean up domain names, which use punycode to handle unicode chars
+ urllist = list(urlsplit(url))
+ netloc = urllist[1]
+ if _has_unicode(netloc):
+ domains = netloc.split(".")
+ for i, d in enumerate(domains):
+ if _has_unicode(d):
+ d = "xn--" + d.encode("punycode").decode("ascii")
+ domains[i] = d
+ urllist[1] = ".".join(domains)
+ url = urlunsplit(urllist)
+
+ # Clean up path/query/params, which use url-encoding to handle unicode chars
+ if isinstance(url.encode("utf8"), six.string_types):
+ url = url.encode("utf8")
+ chars = list(url)
+ for i, x in enumerate(chars):
+ if ord(x) > 128:
+ chars[i] = quote(x)
+
+ return "".join(chars)
+
+
+def _is_redirect(response):
+ try:
+ # 2.0.0 <= requests <= 2.2
+ return response.is_redirect
+
+ except AttributeError:
+ # requests > 2.2
+ return (
+ # use request.sessions conditional
+ response.status_code in REDIRECT_STATI
+ and "location" in response.headers
+ )
+
+
+def _cookies_from_headers(headers):
+ try:
+ import http.cookies as cookies
+
+ resp_cookie = cookies.SimpleCookie()
+ resp_cookie.load(headers["set-cookie"])
+
+ cookies_dict = {name: v.value for name, v in resp_cookie.items()}
+ except ImportError:
+ from cookies import Cookies
+
+ resp_cookies = Cookies.from_request(headers["set-cookie"])
+ cookies_dict = {v.name: v.value for _, v in resp_cookies.items()}
+ return cookiejar_from_dict(cookies_dict)
+
+
+_wrapper_template = """\
+def wrapper%(wrapper_args)s:
+ with responses:
+ return func%(func_args)s
+"""
+
+
+def get_wrapped(func, responses):
+ if six.PY2:
+ args, a, kw, defaults = inspect.getargspec(func)
+ wrapper_args = inspect.formatargspec(args, a, kw, defaults)
+
+ # Preserve the argspec for the wrapped function so that testing
+ # tools such as pytest can continue to use their fixture injection.
+ if hasattr(func, "__self__"):
+ args = args[1:] # Omit 'self'
+ func_args = inspect.formatargspec(args, a, kw, None)
+ else:
+ signature = inspect.signature(func)
+ signature = signature.replace(return_annotation=inspect.Signature.empty)
+ # If the function is wrapped, switch to *args, **kwargs for the parameters
+ # as we can't rely on the signature to give us the arguments the function will
+ # be called with. For example unittest.mock.patch uses required args that are
+ # not actually passed to the function when invoked.
+ if hasattr(func, "__wrapped__"):
+ wrapper_params = [
+ inspect.Parameter("args", inspect.Parameter.VAR_POSITIONAL),
+ inspect.Parameter("kwargs", inspect.Parameter.VAR_KEYWORD),
+ ]
+ else:
+ wrapper_params = [
+ param.replace(annotation=inspect.Parameter.empty)
+ for param in signature.parameters.values()
+ ]
+ signature = signature.replace(parameters=wrapper_params)
+
+ wrapper_args = str(signature)
+ params_without_defaults = [
+ param.replace(
+ annotation=inspect.Parameter.empty, default=inspect.Parameter.empty
+ )
+ for param in signature.parameters.values()
+ ]
+ signature = signature.replace(parameters=params_without_defaults)
+ func_args = str(signature)
+
+ evaldict = {"func": func, "responses": responses}
+ six.exec_(
+ _wrapper_template % {"wrapper_args": wrapper_args, "func_args": func_args},
+ evaldict,
+ )
+ wrapper = evaldict["wrapper"]
+ update_wrapper(wrapper, func)
+ return wrapper
+
+
+class CallList(Sequence, Sized):
+ def __init__(self):
+ self._calls = []
+
+ def __iter__(self):
+ return iter(self._calls)
+
+ def __len__(self):
+ return len(self._calls)
+
+ def __getitem__(self, idx):
+ return self._calls[idx]
+
+ def add(self, request, response):
+ self._calls.append(Call(request, response))
+
+ def reset(self):
+ self._calls = []
+
+
+def _ensure_url_default_path(url):
+ if _is_string(url):
+ url_parts = list(urlsplit(url))
+ if url_parts[2] == "":
+ url_parts[2] = "/"
+ url = urlunsplit(url_parts)
+ return url
+
+
+def _handle_body(body):
+ if isinstance(body, six.text_type):
+ body = body.encode("utf-8")
+ if isinstance(body, _io.BufferedReader):
+ return body
+
+ return BufferIO(body)
+
+
+_unspecified = object()
+
+
+class BaseResponse(object):
+ content_type = None
+ headers = None
+
+ stream = False
+
+ def __init__(self, method, url, match_querystring=_unspecified):
+ self.method = method
+ # ensure the url has a default path set if the url is a string
+ self.url = _ensure_url_default_path(url)
+ self.match_querystring = self._should_match_querystring(match_querystring)
+ self.call_count = 0
+
+ def __eq__(self, other):
+ if not isinstance(other, BaseResponse):
+ return False
+
+ if self.method != other.method:
+ return False
+
+ # Can't simply do a equality check on the objects directly here since __eq__ isn't
+ # implemented for regex. It might seem to work as regex is using a cache to return
+ # the same regex instances, but it doesn't in all cases.
+ self_url = self.url.pattern if isinstance(self.url, Pattern) else self.url
+ other_url = other.url.pattern if isinstance(other.url, Pattern) else other.url
+
+ return self_url == other_url
+
+ def __ne__(self, other):
+ return not self.__eq__(other)
+
+ def _url_matches_strict(self, url, other):
+ url_parsed = urlparse(url)
+ other_parsed = urlparse(other)
+
+ if url_parsed[:3] != other_parsed[:3]:
+ return False
+
+ url_qsl = sorted(parse_qsl(url_parsed.query))
+ other_qsl = sorted(parse_qsl(other_parsed.query))
+
+ if len(url_qsl) != len(other_qsl):
+ return False
+
+ for (a_k, a_v), (b_k, b_v) in zip(url_qsl, other_qsl):
+ if a_k != b_k:
+ return False
+
+ if a_v != b_v:
+ return False
+
+ return True
+
+ def _should_match_querystring(self, match_querystring_argument):
+ if match_querystring_argument is not _unspecified:
+ return match_querystring_argument
+
+ if isinstance(self.url, Pattern):
+ # the old default from <= 0.9.0
+ return False
+
+ return bool(urlparse(self.url).query)
+
+ def _url_matches(self, url, other, match_querystring=False):
+ if _is_string(url):
+ if _has_unicode(url):
+ url = _clean_unicode(url)
+ if not isinstance(other, six.text_type):
+ other = other.encode("ascii").decode("utf8")
+ if match_querystring:
+ return self._url_matches_strict(url, other)
+
+ else:
+ url_without_qs = url.split("?", 1)[0]
+ other_without_qs = other.split("?", 1)[0]
+ return url_without_qs == other_without_qs
+
+ elif isinstance(url, Pattern) and url.match(other):
+ return True
+
+ else:
+ return False
+
+ def get_headers(self):
+ headers = {}
+ if self.content_type is not None:
+ headers["Content-Type"] = self.content_type
+ if self.headers:
+ headers.update(self.headers)
+ return headers
+
+ def get_response(self, request):
+ raise NotImplementedError
+
+ def matches(self, request):
+ if request.method != self.method:
+ return False
+
+ if not self._url_matches(self.url, request.url, self.match_querystring):
+ return False
+
+ return True
+
+
+class Response(BaseResponse):
+ def __init__(
+ self,
+ method,
+ url,
+ body="",
+ json=None,
+ status=200,
+ headers=None,
+ stream=False,
+ content_type=UNSET,
+ **kwargs
+ ):
+ # if we were passed a `json` argument,
+ # override the body and content_type
+ if json is not None:
+ assert not body
+ body = json_module.dumps(json)
+ if content_type is UNSET:
+ content_type = "application/json"
+
+ if content_type is UNSET:
+ content_type = "text/plain"
+
+ # body must be bytes
+ if isinstance(body, six.text_type):
+ body = body.encode("utf-8")
+
+ self.body = body
+ self.status = status
+ self.headers = headers
+ self.stream = stream
+ self.content_type = content_type
+ super(Response, self).__init__(method, url, **kwargs)
+
+ def get_response(self, request):
+ if self.body and isinstance(self.body, Exception):
+ raise self.body
+
+ headers = self.get_headers()
+ status = self.status
+ body = _handle_body(self.body)
+
+ return HTTPResponse(
+ status=status,
+ reason=six.moves.http_client.responses.get(status),
+ body=body,
+ headers=headers,
+ preload_content=False,
+ )
+
+
+class CallbackResponse(BaseResponse):
+ def __init__(
+ self, method, url, callback, stream=False, content_type="text/plain", **kwargs
+ ):
+ self.callback = callback
+ self.stream = stream
+ self.content_type = content_type
+ super(CallbackResponse, self).__init__(method, url, **kwargs)
+
+ def get_response(self, request):
+ headers = self.get_headers()
+
+ result = self.callback(request)
+ if isinstance(result, Exception):
+ raise result
+
+ status, r_headers, body = result
+ if isinstance(body, Exception):
+ raise body
+
+ body = _handle_body(body)
+ headers.update(r_headers)
+
+ return HTTPResponse(
+ status=status,
+ reason=six.moves.http_client.responses.get(status),
+ body=body,
+ headers=headers,
+ preload_content=False,
+ )
+
+
+class RequestsMock(object):
+ DELETE = "DELETE"
+ GET = "GET"
+ HEAD = "HEAD"
+ OPTIONS = "OPTIONS"
+ PATCH = "PATCH"
+ POST = "POST"
+ PUT = "PUT"
+ response_callback = None
+
+ def __init__(
+ self,
+ assert_all_requests_are_fired=True,
+ response_callback=None,
+ passthru_prefixes=(),
+ target="requests.adapters.HTTPAdapter.send",
+ ):
+ self._calls = CallList()
+ self.reset()
+ self.assert_all_requests_are_fired = assert_all_requests_are_fired
+ self.response_callback = response_callback
+ self.passthru_prefixes = tuple(passthru_prefixes)
+ self.target = target
+
+ def reset(self):
+ self._matches = []
+ self._calls.reset()
+
+ def add(
+ self,
+ method=None, # method or ``Response``
+ url=None,
+ body="",
+ adding_headers=None,
+ *args,
+ **kwargs
+ ):
+ """
+ A basic request:
+
+ >>> responses.add(responses.GET, 'http://example.com')
+
+ You can also directly pass an object which implements the
+ ``BaseResponse`` interface:
+
+ >>> responses.add(Response(...))
+
+ A JSON payload:
+
+ >>> responses.add(
+ >>> method='GET',
+ >>> url='http://example.com',
+ >>> json={'foo': 'bar'},
+ >>> )
+
+ Custom headers:
+
+ >>> responses.add(
+ >>> method='GET',
+ >>> url='http://example.com',
+ >>> headers={'X-Header': 'foo'},
+ >>> )
+
+
+ Strict query string matching:
+
+ >>> responses.add(
+ >>> method='GET',
+ >>> url='http://example.com?foo=bar',
+ >>> match_querystring=True
+ >>> )
+ """
+ if isinstance(method, BaseResponse):
+ self._matches.append(method)
+ return
+
+ if adding_headers is not None:
+ kwargs.setdefault("headers", adding_headers)
+
+ self._matches.append(Response(method=method, url=url, body=body, **kwargs))
+
+ def add_passthru(self, prefix):
+ """
+ Register a URL prefix to passthru any non-matching mock requests to.
+
+ For example, to allow any request to 'https://example.com', but require
+ mocks for the remainder, you would add the prefix as so:
+
+ >>> responses.add_passthru('https://example.com')
+ """
+ if _has_unicode(prefix):
+ prefix = _clean_unicode(prefix)
+ self.passthru_prefixes += (prefix,)
+
+ def remove(self, method_or_response=None, url=None):
+ """
+ Removes a response previously added using ``add()``, identified
+ either by a response object inheriting ``BaseResponse`` or
+ ``method`` and ``url``. Removes all matching responses.
+
+ >>> response.add(responses.GET, 'http://example.org')
+ >>> response.remove(responses.GET, 'http://example.org')
+ """
+ if isinstance(method_or_response, BaseResponse):
+ response = method_or_response
+ else:
+ response = BaseResponse(method=method_or_response, url=url)
+
+ while response in self._matches:
+ self._matches.remove(response)
+
+ def replace(self, method_or_response=None, url=None, body="", *args, **kwargs):
+ """
+ Replaces a response previously added using ``add()``. The signature
+ is identical to ``add()``. The response is identified using ``method``
+ and ``url``, and the first matching response is replaced.
+
+ >>> responses.add(responses.GET, 'http://example.org', json={'data': 1})
+ >>> responses.replace(responses.GET, 'http://example.org', json={'data': 2})
+ """
+ if isinstance(method_or_response, BaseResponse):
+ response = method_or_response
+ else:
+ response = Response(method=method_or_response, url=url, body=body, **kwargs)
+
+ index = self._matches.index(response)
+ self._matches[index] = response
+
+ def add_callback(
+ self, method, url, callback, match_querystring=False, content_type="text/plain"
+ ):
+ # ensure the url has a default path set if the url is a string
+ # url = _ensure_url_default_path(url, match_querystring)
+
+ self._matches.append(
+ CallbackResponse(
+ url=url,
+ method=method,
+ callback=callback,
+ content_type=content_type,
+ match_querystring=match_querystring,
+ )
+ )
+
+ @property
+ def calls(self):
+ return self._calls
+
+ def __enter__(self):
+ self.start()
+ return self
+
+ def __exit__(self, type, value, traceback):
+ success = type is None
+ self.stop(allow_assert=success)
+ self.reset()
+ return success
+
+ def activate(self, func):
+ return get_wrapped(func, self)
+
+ def _find_match(self, request):
+ found = None
+ found_match = None
+ for i, match in enumerate(self._matches):
+ if match.matches(request):
+ if found is None:
+ found = i
+ found_match = match
+ else:
+ # Multiple matches found. Remove & return the first match.
+ return self._matches.pop(found)
+
+ return found_match
+
+ def _on_request(self, adapter, request, **kwargs):
+ match = self._find_match(request)
+ resp_callback = self.response_callback
+
+ if match is None:
+ if request.url.startswith(self.passthru_prefixes):
+ logger.info("request.allowed-passthru", extra={"url": request.url})
+ return _real_send(adapter, request, **kwargs)
+
+ error_msg = (
+ "Connection refused by Responses: {0} {1} doesn't "
+ "match Responses Mock".format(request.method, request.url)
+ )
+ response = ConnectionError(error_msg)
+ response.request = request
+
+ self._calls.add(request, response)
+ response = resp_callback(response) if resp_callback else response
+ raise response
+
+ try:
+ response = adapter.build_response(request, match.get_response(request))
+ except Exception as response:
+ match.call_count += 1
+ self._calls.add(request, response)
+ response = resp_callback(response) if resp_callback else response
+ raise
+
+ if not match.stream:
+ response.content # NOQA
+
+ try:
+ response.cookies = _cookies_from_headers(response.headers)
+ except (KeyError, TypeError):
+ pass
+
+ response = resp_callback(response) if resp_callback else response
+ match.call_count += 1
+ self._calls.add(request, response)
+ return response
+
+ def start(self):
+ def unbound_on_send(adapter, request, *a, **kwargs):
+ return self._on_request(adapter, request, *a, **kwargs)
+
+ self._patcher = std_mock.patch(target=self.target, new=unbound_on_send)
+ self._patcher.start()
+
+ def stop(self, allow_assert=True):
+ self._patcher.stop()
+ if not self.assert_all_requests_are_fired:
+ return
+
+ if not allow_assert:
+ return
+
+ not_called = [m for m in self._matches if m.call_count == 0]
+ if not_called:
+ raise AssertionError(
+ "Not all requests have been executed {0!r}".format(
+ [(match.method, match.url) for match in not_called]
+ )
+ )
+
+
+# expose default mock namespace
+mock = _default_mock = RequestsMock(assert_all_requests_are_fired=False)
+__all__ = ["CallbackResponse", "Response", "RequestsMock"]
+for __attr in (a for a in dir(_default_mock) if not a.startswith("_")):
+ __all__.append(__attr)
+ globals()[__attr] = getattr(_default_mock, __attr)
diff --git a/third_party/python/responses/setup.cfg b/third_party/python/responses/setup.cfg
new file mode 100644
index 0000000000..79de8cb6e5
--- /dev/null
+++ b/third_party/python/responses/setup.cfg
@@ -0,0 +1,16 @@
+[tool:pytest]
+addopts = --tb=short
+
+[bdist_wheel]
+universal = 1
+
+[flake8]
+max-line-length = 100
+
+[yapf]
+based_on_style = pep8
+
+[egg_info]
+tag_build =
+tag_date = 0
+
diff --git a/third_party/python/responses/setup.py b/third_party/python/responses/setup.py
new file mode 100644
index 0000000000..779aa32f40
--- /dev/null
+++ b/third_party/python/responses/setup.py
@@ -0,0 +1,85 @@
+#!/usr/bin/env python
+"""
+responses
+=========
+
+A utility library for mocking out the `requests` Python library.
+
+:copyright: (c) 2015 David Cramer
+:license: Apache 2.0
+"""
+
+import sys
+
+from setuptools import setup
+from setuptools.command.test import test as TestCommand
+
+setup_requires = []
+
+if "test" in sys.argv:
+ setup_requires.append("pytest")
+
+install_requires = [
+ "cookies; python_version < '3.4'",
+ "mock; python_version < '3.3'",
+ "requests>=2.0",
+ "six",
+]
+
+tests_require = [
+ "pytest",
+ "coverage >= 3.7.1, < 5.0.0",
+ "pytest-cov",
+ "pytest-localserver",
+ "flake8",
+]
+
+extras_require = {"tests": tests_require}
+
+
+class PyTest(TestCommand):
+ def finalize_options(self):
+ TestCommand.finalize_options(self)
+ self.test_args = ["test_responses.py"]
+ self.test_suite = True
+
+ def run_tests(self):
+ # import here, cause outside the eggs aren't loaded
+ import pytest
+
+ errno = pytest.main(self.test_args)
+ sys.exit(errno)
+
+
+setup(
+ name="responses",
+ version="0.10.6",
+ author="David Cramer",
+ description=("A utility library for mocking out the `requests` Python library."),
+ url="https://github.com/getsentry/responses",
+ license="Apache 2.0",
+ long_description=open("README.rst").read(),
+ py_modules=["responses"],
+ zip_safe=False,
+ python_requires=">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*",
+ install_requires=install_requires,
+ extras_require=extras_require,
+ tests_require=tests_require,
+ setup_requires=setup_requires,
+ cmdclass={"test": PyTest},
+ include_package_data=True,
+ classifiers=[
+ "Intended Audience :: Developers",
+ "Intended Audience :: System Administrators",
+ "Operating System :: OS Independent",
+ "Programming Language :: Python",
+ "Programming Language :: Python :: 2",
+ "Programming Language :: Python :: 2.7",
+ "Programming Language :: Python :: 3",
+ "Programming Language :: Python :: 3.4",
+ "Programming Language :: Python :: 3.5",
+ "Programming Language :: Python :: 3.6",
+ "Programming Language :: Python :: 3.7",
+ "Topic :: Software Development",
+ ],
+)
diff --git a/third_party/python/responses/test_responses.py b/third_party/python/responses/test_responses.py
new file mode 100644
index 0000000000..0350e49538
--- /dev/null
+++ b/third_party/python/responses/test_responses.py
@@ -0,0 +1,924 @@
+# coding: utf-8
+
+from __future__ import absolute_import, print_function, division, unicode_literals
+
+import inspect
+import re
+import six
+
+import pytest
+import requests
+import responses
+from requests.exceptions import ConnectionError, HTTPError
+from responses import BaseResponse, Response
+
+try:
+ from mock import patch, Mock
+except ImportError:
+ from unittest.mock import patch, Mock
+
+
+def assert_reset():
+ assert len(responses._default_mock._matches) == 0
+ assert len(responses.calls) == 0
+
+
+def assert_response(resp, body=None, content_type="text/plain"):
+ assert resp.status_code == 200
+ assert resp.reason == "OK"
+ if content_type is not None:
+ assert resp.headers["Content-Type"] == content_type
+ else:
+ assert "Content-Type" not in resp.headers
+ assert resp.text == body
+
+
+def test_response():
+ @responses.activate
+ def run():
+ responses.add(responses.GET, "http://example.com", body=b"test")
+ resp = requests.get("http://example.com")
+ assert_response(resp, "test")
+ assert len(responses.calls) == 1
+ assert responses.calls[0].request.url == "http://example.com/"
+ assert responses.calls[0].response.content == b"test"
+
+ resp = requests.get("http://example.com?foo=bar")
+ assert_response(resp, "test")
+ assert len(responses.calls) == 2
+ assert responses.calls[1].request.url == "http://example.com/?foo=bar"
+ assert responses.calls[1].response.content == b"test"
+
+ run()
+ assert_reset()
+
+
+def test_response_encoded():
+ @responses.activate
+ def run():
+ # Path contains urlencoded =/()[]
+ url = "http://example.org/foo.bar%3D%2F%28%29%5B%5D"
+ responses.add(responses.GET, url, body="it works", status=200)
+ resp = requests.get(url)
+ assert_response(resp, "it works")
+
+ run()
+ assert_reset()
+
+
+def test_response_with_instance():
+ @responses.activate
+ def run():
+ responses.add(
+ responses.Response(method=responses.GET, url="http://example.com")
+ )
+ resp = requests.get("http://example.com")
+ assert_response(resp, "")
+ assert len(responses.calls) == 1
+ assert responses.calls[0].request.url == "http://example.com/"
+
+ resp = requests.get("http://example.com?foo=bar")
+ assert_response(resp, "")
+ assert len(responses.calls) == 2
+ assert responses.calls[1].request.url == "http://example.com/?foo=bar"
+
+
+@pytest.mark.parametrize(
+ "original,replacement",
+ [
+ ("http://example.com/two", "http://example.com/two"),
+ (
+ Response(method=responses.GET, url="http://example.com/two"),
+ Response(
+ method=responses.GET, url="http://example.com/two", body="testtwo"
+ ),
+ ),
+ (
+ re.compile(r"http://example\.com/two"),
+ re.compile(r"http://example\.com/two"),
+ ),
+ ],
+)
+def test_replace(original, replacement):
+ @responses.activate
+ def run():
+ responses.add(responses.GET, "http://example.com/one", body="test1")
+
+ if isinstance(original, BaseResponse):
+ responses.add(original)
+ else:
+ responses.add(responses.GET, original, body="test2")
+
+ responses.add(responses.GET, "http://example.com/three", body="test3")
+ responses.add(
+ responses.GET, re.compile(r"http://example\.com/four"), body="test3"
+ )
+
+ if isinstance(replacement, BaseResponse):
+ responses.replace(replacement)
+ else:
+ responses.replace(responses.GET, replacement, body="testtwo")
+
+ resp = requests.get("http://example.com/two")
+ assert_response(resp, "testtwo")
+
+ run()
+ assert_reset()
+
+
+@pytest.mark.parametrize(
+ "original,replacement",
+ [
+ ("http://example.com/one", re.compile(r"http://example\.com/one")),
+ (re.compile(r"http://example\.com/one"), "http://example.com/one"),
+ ],
+)
+def test_replace_error(original, replacement):
+ @responses.activate
+ def run():
+ responses.add(responses.GET, original)
+ with pytest.raises(ValueError):
+ responses.replace(responses.GET, replacement)
+
+ run()
+ assert_reset()
+
+
+def test_remove():
+ @responses.activate
+ def run():
+ responses.add(responses.GET, "http://example.com/zero")
+ responses.add(responses.GET, "http://example.com/one")
+ responses.add(responses.GET, "http://example.com/two")
+ responses.add(responses.GET, re.compile(r"http://example\.com/three"))
+ responses.add(responses.GET, re.compile(r"http://example\.com/four"))
+ re.purge()
+ responses.remove(responses.GET, "http://example.com/two")
+ responses.remove(Response(method=responses.GET, url="http://example.com/zero"))
+ responses.remove(responses.GET, re.compile(r"http://example\.com/four"))
+
+ with pytest.raises(ConnectionError):
+ requests.get("http://example.com/zero")
+ requests.get("http://example.com/one")
+ with pytest.raises(ConnectionError):
+ requests.get("http://example.com/two")
+ requests.get("http://example.com/three")
+ with pytest.raises(ConnectionError):
+ requests.get("http://example.com/four")
+
+ run()
+ assert_reset()
+
+
+@pytest.mark.parametrize(
+ "args1,kwargs1,args2,kwargs2,expected",
+ [
+ ((responses.GET, "a"), {}, (responses.GET, "a"), {}, True),
+ ((responses.GET, "a"), {}, (responses.GET, "b"), {}, False),
+ ((responses.GET, "a"), {}, (responses.POST, "a"), {}, False),
+ (
+ (responses.GET, "a"),
+ {"match_querystring": True},
+ (responses.GET, "a"),
+ {},
+ True,
+ ),
+ ],
+)
+def test_response_equality(args1, kwargs1, args2, kwargs2, expected):
+ o1 = BaseResponse(*args1, **kwargs1)
+ o2 = BaseResponse(*args2, **kwargs2)
+ assert (o1 == o2) is expected
+ assert (o1 != o2) is not expected
+
+
+def test_response_equality_different_objects():
+ o1 = BaseResponse(method=responses.GET, url="a")
+ o2 = "str"
+ assert (o1 == o2) is False
+ assert (o1 != o2) is True
+
+
+def test_connection_error():
+ @responses.activate
+ def run():
+ responses.add(responses.GET, "http://example.com")
+
+ with pytest.raises(ConnectionError):
+ requests.get("http://example.com/foo")
+
+ assert len(responses.calls) == 1
+ assert responses.calls[0].request.url == "http://example.com/foo"
+ assert type(responses.calls[0].response) is ConnectionError
+ assert responses.calls[0].response.request
+
+ run()
+ assert_reset()
+
+
+def test_match_querystring():
+ @responses.activate
+ def run():
+ url = "http://example.com?test=1&foo=bar"
+ responses.add(responses.GET, url, match_querystring=True, body=b"test")
+ resp = requests.get("http://example.com?test=1&foo=bar")
+ assert_response(resp, "test")
+ resp = requests.get("http://example.com?foo=bar&test=1")
+ assert_response(resp, "test")
+ resp = requests.get("http://example.com/?foo=bar&test=1")
+ assert_response(resp, "test")
+
+ run()
+ assert_reset()
+
+
+def test_match_empty_querystring():
+ @responses.activate
+ def run():
+ responses.add(
+ responses.GET, "http://example.com", body=b"test", match_querystring=True
+ )
+ resp = requests.get("http://example.com")
+ assert_response(resp, "test")
+ resp = requests.get("http://example.com/")
+ assert_response(resp, "test")
+ with pytest.raises(ConnectionError):
+ requests.get("http://example.com?query=foo")
+
+ run()
+ assert_reset()
+
+
+def test_match_querystring_error():
+ @responses.activate
+ def run():
+ responses.add(
+ responses.GET, "http://example.com/?test=1", match_querystring=True
+ )
+
+ with pytest.raises(ConnectionError):
+ requests.get("http://example.com/foo/?test=2")
+
+ run()
+ assert_reset()
+
+
+def test_match_querystring_regex():
+ @responses.activate
+ def run():
+ """Note that `match_querystring` value shouldn't matter when passing a
+ regular expression"""
+
+ responses.add(
+ responses.GET,
+ re.compile(r"http://example\.com/foo/\?test=1"),
+ body="test1",
+ match_querystring=True,
+ )
+
+ resp = requests.get("http://example.com/foo/?test=1")
+ assert_response(resp, "test1")
+
+ responses.add(
+ responses.GET,
+ re.compile(r"http://example\.com/foo/\?test=2"),
+ body="test2",
+ match_querystring=False,
+ )
+
+ resp = requests.get("http://example.com/foo/?test=2")
+ assert_response(resp, "test2")
+
+ run()
+ assert_reset()
+
+
+def test_match_querystring_error_regex():
+ @responses.activate
+ def run():
+ """Note that `match_querystring` value shouldn't matter when passing a
+ regular expression"""
+
+ responses.add(
+ responses.GET,
+ re.compile(r"http://example\.com/foo/\?test=1"),
+ match_querystring=True,
+ )
+
+ with pytest.raises(ConnectionError):
+ requests.get("http://example.com/foo/?test=3")
+
+ responses.add(
+ responses.GET,
+ re.compile(r"http://example\.com/foo/\?test=2"),
+ match_querystring=False,
+ )
+
+ with pytest.raises(ConnectionError):
+ requests.get("http://example.com/foo/?test=4")
+
+ run()
+ assert_reset()
+
+
+def test_match_querystring_auto_activates():
+ @responses.activate
+ def run():
+ responses.add(responses.GET, "http://example.com?test=1", body=b"test")
+ resp = requests.get("http://example.com?test=1")
+ assert_response(resp, "test")
+ with pytest.raises(ConnectionError):
+ requests.get("http://example.com/?test=2")
+
+ run()
+ assert_reset()
+
+
+def test_accept_string_body():
+ @responses.activate
+ def run():
+ url = "http://example.com/"
+ responses.add(responses.GET, url, body="test")
+ resp = requests.get(url)
+ assert_response(resp, "test")
+
+ run()
+ assert_reset()
+
+
+def test_accept_json_body():
+ @responses.activate
+ def run():
+ content_type = "application/json"
+
+ url = "http://example.com/"
+ responses.add(responses.GET, url, json={"message": "success"})
+ resp = requests.get(url)
+ assert_response(resp, '{"message": "success"}', content_type)
+
+ url = "http://example.com/1/"
+ responses.add(responses.GET, url, json=[])
+ resp = requests.get(url)
+ assert_response(resp, "[]", content_type)
+
+ run()
+ assert_reset()
+
+
+def test_no_content_type():
+ @responses.activate
+ def run():
+ url = "http://example.com/"
+ responses.add(responses.GET, url, body="test", content_type=None)
+ resp = requests.get(url)
+ assert_response(resp, "test", content_type=None)
+
+ run()
+ assert_reset()
+
+
+def test_arbitrary_status_code():
+ @responses.activate
+ def run():
+ url = "http://example.com/"
+ responses.add(responses.GET, url, body="test", status=418)
+ resp = requests.get(url)
+ assert resp.status_code == 418
+ assert resp.reason is None
+
+ run()
+ assert_reset()
+
+
+def test_throw_connection_error_explicit():
+ @responses.activate
+ def run():
+ url = "http://example.com"
+ exception = HTTPError("HTTP Error")
+ responses.add(responses.GET, url, exception)
+
+ with pytest.raises(HTTPError) as HE:
+ requests.get(url)
+
+ assert str(HE.value) == "HTTP Error"
+
+ run()
+ assert_reset()
+
+
+def test_callback():
+ body = b"test callback"
+ status = 400
+ reason = "Bad Request"
+ headers = {"foo": "bar"}
+ url = "http://example.com/"
+
+ def request_callback(request):
+ return (status, headers, body)
+
+ @responses.activate
+ def run():
+ responses.add_callback(responses.GET, url, request_callback)
+ resp = requests.get(url)
+ assert resp.text == "test callback"
+ assert resp.status_code == status
+ assert resp.reason == reason
+ assert "foo" in resp.headers
+ assert resp.headers["foo"] == "bar"
+
+ run()
+ assert_reset()
+
+
+def test_callback_exception_result():
+ result = Exception()
+ url = "http://example.com/"
+
+ def request_callback(request):
+ return result
+
+ @responses.activate
+ def run():
+ responses.add_callback(responses.GET, url, request_callback)
+
+ with pytest.raises(Exception) as e:
+ requests.get(url)
+
+ assert e.value is result
+
+ run()
+ assert_reset()
+
+
+def test_callback_exception_body():
+ body = Exception()
+ url = "http://example.com/"
+
+ def request_callback(request):
+ return (200, {}, body)
+
+ @responses.activate
+ def run():
+ responses.add_callback(responses.GET, url, request_callback)
+
+ with pytest.raises(Exception) as e:
+ requests.get(url)
+
+ assert e.value is body
+
+ run()
+ assert_reset()
+
+
+def test_callback_no_content_type():
+ body = b"test callback"
+ status = 400
+ reason = "Bad Request"
+ headers = {"foo": "bar"}
+ url = "http://example.com/"
+
+ def request_callback(request):
+ return (status, headers, body)
+
+ @responses.activate
+ def run():
+ responses.add_callback(responses.GET, url, request_callback, content_type=None)
+ resp = requests.get(url)
+ assert resp.text == "test callback"
+ assert resp.status_code == status
+ assert resp.reason == reason
+ assert "foo" in resp.headers
+ assert "Content-Type" not in resp.headers
+
+ run()
+ assert_reset()
+
+
+def test_regular_expression_url():
+ @responses.activate
+ def run():
+ url = re.compile(r"https?://(.*\.)?example.com")
+ responses.add(responses.GET, url, body=b"test")
+
+ resp = requests.get("http://example.com")
+ assert_response(resp, "test")
+
+ resp = requests.get("https://example.com")
+ assert_response(resp, "test")
+
+ resp = requests.get("https://uk.example.com")
+ assert_response(resp, "test")
+
+ with pytest.raises(ConnectionError):
+ requests.get("https://uk.exaaample.com")
+
+ run()
+ assert_reset()
+
+
+def test_custom_adapter():
+ @responses.activate
+ def run():
+ url = "http://example.com"
+ responses.add(responses.GET, url, body=b"test")
+
+ calls = [0]
+
+ class DummyAdapter(requests.adapters.HTTPAdapter):
+ def send(self, *a, **k):
+ calls[0] += 1
+ return super(DummyAdapter, self).send(*a, **k)
+
+ # Test that the adapter is actually used
+ session = requests.Session()
+ session.mount("http://", DummyAdapter())
+
+ resp = session.get(url, allow_redirects=False)
+ assert calls[0] == 1
+
+ # Test that the response is still correctly emulated
+ session = requests.Session()
+ session.mount("http://", DummyAdapter())
+
+ resp = session.get(url)
+ assert_response(resp, "test")
+
+ run()
+
+
+def test_responses_as_context_manager():
+ def run():
+ with responses.mock:
+ responses.add(responses.GET, "http://example.com", body=b"test")
+ resp = requests.get("http://example.com")
+ assert_response(resp, "test")
+ assert len(responses.calls) == 1
+ assert responses.calls[0].request.url == "http://example.com/"
+ assert responses.calls[0].response.content == b"test"
+
+ resp = requests.get("http://example.com?foo=bar")
+ assert_response(resp, "test")
+ assert len(responses.calls) == 2
+ assert responses.calls[1].request.url == "http://example.com/?foo=bar"
+ assert responses.calls[1].response.content == b"test"
+
+ run()
+ assert_reset()
+
+
+def test_activate_doesnt_change_signature():
+ def test_function(a, b=None):
+ return (a, b)
+
+ decorated_test_function = responses.activate(test_function)
+ if hasattr(inspect, "signature"):
+ assert inspect.signature(test_function) == inspect.signature(
+ decorated_test_function
+ )
+ else:
+ assert inspect.getargspec(test_function) == inspect.getargspec(
+ decorated_test_function
+ )
+ assert decorated_test_function(1, 2) == test_function(1, 2)
+ assert decorated_test_function(3) == test_function(3)
+
+
+def test_activate_mock_interaction():
+ @patch("sys.stdout")
+ def test_function(mock_stdout):
+ return mock_stdout
+
+ decorated_test_function = responses.activate(test_function)
+ if hasattr(inspect, "signature"):
+ assert inspect.signature(test_function) == inspect.signature(
+ decorated_test_function
+ )
+ else:
+ assert inspect.getargspec(test_function) == inspect.getargspec(
+ decorated_test_function
+ )
+
+ value = test_function()
+ assert isinstance(value, Mock)
+
+ value = decorated_test_function()
+ assert isinstance(value, Mock)
+
+
+@pytest.mark.skipif(six.PY2, reason="Cannot run in python2")
+def test_activate_doesnt_change_signature_with_return_type():
+ def test_function(a, b=None):
+ return (a, b)
+
+ # Add type annotations as they are syntax errors in py2.
+ # Use a class to test for import errors in evaled code.
+ test_function.__annotations__["return"] = Mock
+ test_function.__annotations__["a"] = Mock
+
+ decorated_test_function = responses.activate(test_function)
+ if hasattr(inspect, "signature"):
+ assert inspect.signature(test_function) == inspect.signature(
+ decorated_test_function
+ )
+ else:
+ assert inspect.getargspec(test_function) == inspect.getargspec(
+ decorated_test_function
+ )
+ assert decorated_test_function(1, 2) == test_function(1, 2)
+ assert decorated_test_function(3) == test_function(3)
+
+
+def test_activate_doesnt_change_signature_for_method():
+ class TestCase(object):
+ def test_function(self, a, b=None):
+ return (self, a, b)
+
+ decorated_test_function = responses.activate(test_function)
+
+ test_case = TestCase()
+ assert test_case.decorated_test_function(1, 2) == test_case.test_function(1, 2)
+ assert test_case.decorated_test_function(3) == test_case.test_function(3)
+
+
+def test_response_cookies():
+ body = b"test callback"
+ status = 200
+ headers = {"set-cookie": "session_id=12345; a=b; c=d"}
+ url = "http://example.com/"
+
+ def request_callback(request):
+ return (status, headers, body)
+
+ @responses.activate
+ def run():
+ responses.add_callback(responses.GET, url, request_callback)
+ resp = requests.get(url)
+ assert resp.text == "test callback"
+ assert resp.status_code == status
+ assert "session_id" in resp.cookies
+ assert resp.cookies["session_id"] == "12345"
+ assert resp.cookies["a"] == "b"
+ assert resp.cookies["c"] == "d"
+
+ run()
+ assert_reset()
+
+
+def test_response_callback():
+ """adds a callback to decorate the response, then checks it"""
+
+ def run():
+ def response_callback(resp):
+ resp._is_mocked = 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, "_is_mocked")
+ assert resp._is_mocked is True
+
+ run()
+ assert_reset()
+
+
+def test_response_filebody():
+ """ Adds the possibility to use actual (binary) files as responses """
+
+ def run():
+ with responses.RequestsMock() as m:
+ with open("README.rst", "rb") as out:
+ m.add(responses.GET, "http://example.com", body=out, stream=True)
+ resp = requests.get("http://example.com")
+ with open("README.rst", "r") as out:
+ assert resp.text == out.read()
+
+
+def test_assert_all_requests_are_fired():
+ def run():
+ with pytest.raises(AssertionError) as excinfo:
+ with responses.RequestsMock(assert_all_requests_are_fired=True) as m:
+ m.add(responses.GET, "http://example.com", body=b"test")
+ assert "http://example.com" in str(excinfo.value)
+ assert responses.GET in str(excinfo)
+
+ # check that assert_all_requests_are_fired default to True
+ with pytest.raises(AssertionError):
+ with responses.RequestsMock() as m:
+ m.add(responses.GET, "http://example.com", body=b"test")
+
+ # check that assert_all_requests_are_fired doesn't swallow exceptions
+ with pytest.raises(ValueError):
+ with responses.RequestsMock() as m:
+ m.add(responses.GET, "http://example.com", body=b"test")
+ raise ValueError()
+
+ # check that assert_all_requests_are_fired=True doesn't remove urls
+ with responses.RequestsMock(assert_all_requests_are_fired=True) as m:
+ m.add(responses.GET, "http://example.com", body=b"test")
+ assert len(m._matches) == 1
+ requests.get("http://example.com")
+ assert len(m._matches) == 1
+
+ # check that assert_all_requests_are_fired=True counts mocked errors
+ with responses.RequestsMock(assert_all_requests_are_fired=True) as m:
+ m.add(responses.GET, "http://example.com", body=Exception())
+ assert len(m._matches) == 1
+ with pytest.raises(Exception):
+ requests.get("http://example.com")
+ assert len(m._matches) == 1
+
+ run()
+ assert_reset()
+
+
+def test_allow_redirects_samehost():
+ redirecting_url = "http://example.com"
+ final_url_path = "/1"
+ final_url = "{0}{1}".format(redirecting_url, final_url_path)
+ url_re = re.compile(r"^http://example.com(/)?(\d+)?$")
+
+ def request_callback(request):
+ # endpoint of chained redirect
+ if request.url.endswith(final_url_path):
+ return 200, (), b"test"
+
+ # otherwise redirect to an integer path
+ else:
+ if request.url.endswith("/0"):
+ n = 1
+ else:
+ n = 0
+ redirect_headers = {"location": "/{0!s}".format(n)}
+ return 301, redirect_headers, None
+
+ def run():
+ # setup redirect
+ with responses.mock:
+ responses.add_callback(responses.GET, url_re, request_callback)
+ resp_no_redirects = requests.get(redirecting_url, allow_redirects=False)
+ assert resp_no_redirects.status_code == 301
+ assert len(responses.calls) == 1 # 1x300
+ assert responses.calls[0][1].status_code == 301
+ assert_reset()
+
+ with responses.mock:
+ responses.add_callback(responses.GET, url_re, request_callback)
+ resp_yes_redirects = requests.get(redirecting_url, allow_redirects=True)
+ assert len(responses.calls) == 3 # 2x300 + 1x200
+ assert len(resp_yes_redirects.history) == 2
+ assert resp_yes_redirects.status_code == 200
+ assert final_url == resp_yes_redirects.url
+ status_codes = [call[1].status_code for call in responses.calls]
+ assert status_codes == [301, 301, 200]
+ assert_reset()
+
+ run()
+ assert_reset()
+
+
+def test_handles_unicode_querystring():
+ url = "http://example.com/test?type=2&ie=utf8&query=汉字"
+
+ @responses.activate
+ def run():
+ responses.add(responses.GET, url, body="test", match_querystring=True)
+
+ resp = requests.get(url)
+
+ assert_response(resp, "test")
+
+ run()
+ assert_reset()
+
+
+def test_handles_unicode_url():
+ url = "http://www.संजाल.भारत/hi/वेबसाइट-डिजाइन"
+
+ @responses.activate
+ def run():
+ responses.add(responses.GET, url, body="test")
+
+ resp = requests.get(url)
+
+ assert_response(resp, "test")
+
+ run()
+ assert_reset()
+
+
+def test_headers():
+ @responses.activate
+ def run():
+ responses.add(
+ responses.GET, "http://example.com", body="", headers={"X-Test": "foo"}
+ )
+ resp = requests.get("http://example.com")
+ assert resp.headers["X-Test"] == "foo"
+
+ run()
+ assert_reset()
+
+
+def test_legacy_adding_headers():
+ @responses.activate
+ def run():
+ responses.add(
+ responses.GET,
+ "http://example.com",
+ body="",
+ adding_headers={"X-Test": "foo"},
+ )
+ resp = requests.get("http://example.com")
+ assert resp.headers["X-Test"] == "foo"
+
+ run()
+ assert_reset()
+
+
+def test_multiple_responses():
+ @responses.activate
+ def run():
+ responses.add(responses.GET, "http://example.com", body="test")
+ responses.add(responses.GET, "http://example.com", body="rest")
+
+ resp = requests.get("http://example.com")
+ assert_response(resp, "test")
+ resp = requests.get("http://example.com")
+ assert_response(resp, "rest")
+ # After all responses are used, last response should be repeated
+ resp = requests.get("http://example.com")
+ assert_response(resp, "rest")
+
+ run()
+ assert_reset()
+
+
+def test_multiple_urls():
+ @responses.activate
+ def run():
+ responses.add(responses.GET, "http://example.com/one", body="one")
+ responses.add(responses.GET, "http://example.com/two", body="two")
+
+ resp = requests.get("http://example.com/two")
+ assert_response(resp, "two")
+ resp = requests.get("http://example.com/one")
+ assert_response(resp, "one")
+
+ run()
+ assert_reset()
+
+
+def test_passthru(httpserver):
+ httpserver.serve_content("OK", headers={"Content-Type": "text/plain"})
+
+ @responses.activate
+ def run():
+ responses.add_passthru(httpserver.url)
+ responses.add(responses.GET, "{}/one".format(httpserver.url), body="one")
+ responses.add(responses.GET, "http://example.com/two", body="two")
+
+ resp = requests.get("http://example.com/two")
+ assert_response(resp, "two")
+ resp = requests.get("{}/one".format(httpserver.url))
+ assert_response(resp, "one")
+ resp = requests.get(httpserver.url)
+ assert_response(resp, "OK")
+
+ run()
+ assert_reset()
+
+
+def test_method_named_param():
+ @responses.activate
+ def run():
+ responses.add(method=responses.GET, url="http://example.com", body="OK")
+ resp = requests.get("http://example.com")
+ assert_response(resp, "OK")
+
+ run()
+ assert_reset()
+
+
+def test_passthru_unicode():
+ @responses.activate
+ def run():
+ with responses.RequestsMock() as m:
+ url = "http://موقع.وزارة-الاتصالات.مصر/"
+ clean_url = "http://xn--4gbrim.xn----ymcbaaajlc6dj7bxne2c.xn--wgbh1c/"
+ m.add_passthru(url)
+ assert m.passthru_prefixes[0] == clean_url
+
+ run()
+ assert_reset()
+
+
+def test_custom_target(monkeypatch):
+ requests_mock = responses.RequestsMock(target="something.else")
+ std_mock_mock = responses.std_mock.MagicMock()
+ patch_mock = std_mock_mock.patch
+ monkeypatch.setattr(responses, "std_mock", std_mock_mock)
+ requests_mock.start()
+ assert len(patch_mock.call_args_list) == 1
+ assert patch_mock.call_args[1]["target"] == "something.else"
diff --git a/third_party/python/responses/tox.ini b/third_party/python/responses/tox.ini
new file mode 100644
index 0000000000..94d030cc37
--- /dev/null
+++ b/third_party/python/responses/tox.ini
@@ -0,0 +1,7 @@
+[tox]
+envlist = py27,py34,py35,py36,py37
+
+[testenv]
+extras = tests
+commands =
+ pytest . --cov responses --cov-report term-missing