././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1642535106.8147693 responses-0.18.0/0000755000175100001710000000000000000000000013161 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1642535104.0 responses-0.18.0/CHANGES0000644000175100001710000002256400000000000014165 0ustar00runnerdocker0.18.0 ------ * Dropped support of Python 2.7, 3.5, 3.6 * Fixed issue with type annotation for `responses.activate` decorator. See #468 * Removed internal `_is_string` and `_ensure_str` functions * Removed internal `_quote` from `test_responses.py` * Removed internal `_matches` attribute of `RequestsMock` object. * Generated decorator wrapper now uses stdlib features instead of strings and exec * Fix issue when Deprecation Warning was raised with default arguments in `responses.add_callback` due to `match_querystring`. See #464 0.17.0 ------ * This release is the last to support Python 2.7. * Fixed issue when `response.iter_content` when `chunk_size=None` entered infinite loop * Fixed issue when `passthru_prefixes` persisted across tests. Now `add_passthru` is valid only within a context manager or for a single function and cleared on exit * Deprecate `match_querystring` argument in `Response` and `CallbackResponse`. Use `responses.matchers.query_param_matcher` or `responses.matchers.query_string_matcher` * Added support for non-UTF-8 bytes in `responses.matchers.multipart_matcher` * Added `responses.registries`. Now user can create custom registries to manipulate the order of responses in the match algorithm `responses.activate(registry=CustomRegistry)` * Fixed issue with response match when requests were performed between adding responses with same URL. See Issue #212 0.16.0 ------ * Fixed regression with `stream` parameter deprecation, requests.session() and cookie handling. * Replaced adhoc URL parsing with `urllib.parse`. * Added ``match`` parameter to ``add_callback`` method * Added `responses.matchers.fragment_identifier_matcher`. This matcher allows you to match request URL fragment identifier. * Improved test coverage. * Fixed failing test in python 2.7 when `python-future` is also installed. 0.15.0 ------ * Added `responses.PassthroughResponse` and `reponses.BaseResponse.passthrough`. These features make building passthrough responses more compatible with dynamcially generated response objects. * Removed the unused ``_is_redirect()`` function from responses internals. * Added `responses.matchers.request_kwargs_matcher`. This matcher allows you to match additional request arguments like `stream`. * Added `responses.matchers.multipart_matcher`. This matcher allows you to match request body and headers for ``multipart/form-data`` data * Added `responses.matchers.query_string_matcher`. This matcher allows you to match request query string, similar to `responses.matchers.query_param_matcher`. * Added `responses.matchers.header_matcher()`. This matcher allows you to match request headers. By default only headers supplied to `header_matcher()` are checked. You can make header matching exhaustive by passing `strict_match=True` to `header_matcher()`. * Changed all matchers output message in case of mismatch. Now message is aligned between Python2 and Python3 versions * Deprecate ``stream`` argument in ``Response`` and ``CallbackResponse`` * Added Python 3.10 support 0.14.0 ------ * Added `responses.matchers`. * Moved `responses.json_params_matcher` to `responses.matchers.json_params_matcher` * Moved `responses.urlencoded_params_matcher` to `responses.matchers.urlencoded_params_matcher` * Added `responses.matchers.query_param_matcher`. This matcher allows you to match query strings with a dictionary. * Added `auto_calculate_content_length` option to `responses.add()`. When enabled, this option will generate a `Content-Length` header based on the number of bytes in the response body. 0.13.4 ------ * Improve typing support * Use URLs with normalized hostnames when comparing URLs. 0.13.3 ------ * Switch from Travis to GHA for deployment. 0.13.2 ------ * Fixed incorrect type stubs for `add_callback` 0.13.1 ------ * Fixed packages not containing type stubs. 0.13.0 ------ * `responses.upsert()` was added. This method will `add()` a response if one has not already been registered for a URL, or `replace()` an existing response. * `responses.registered()` was added. The method allows you to get a list of the currently registered responses. This formalizes the previously private `responses.mock._matches` method. * A more useful `__repr__` has been added to `Response`. * Error messages have been improved. 0.12.1 ------ * `responses.urlencoded_params_matcher` and `responses.json_params_matcher` now accept None to match empty requests. * Fixed imports to work with new `urllib3` versions. * `request.params` now allows parameters to have multiple values for the same key. * Improved ConnectionError messages. 0.12.0 ------ - Remove support for Python 3.4. 0.11.0 ------ - Added the `match` parameter to `add()`. - Added `responses.urlencoded_params_matcher()` and `responses.json_params_matcher()`. 0.10.16 ------- - Add a requirements pin to urllib3. This helps prevent broken install states where cookie usage fails. 0.10.15 ------- - Added `assert_call_count` to improve ergonomics around ensuring a mock was called. - Fix incorrect handling of paths with query strings. - Add Python 3.9 support to CI matrix. 0.10.14 ------- - Retag of 0.10.13 0.10.13 ------- - Improved README examples. - Improved handling of unicode bodies. The inferred content-type for unicode bodies is now `text/plain; charset=utf-8`. - Streamlined querysting matching code. 0.10.12 ------- - Fixed incorrect content-type in `add_callback()` when headers are provided as a list of tuples. 0.10.11 ------- - Fixed invalid README formatted. - Fixed string formatting in error message. 0.10.10 ------ - Added Python 3.8 support - Remove Python 3.4 from test suite matrix. - The `response.request` object now has a `params` attribute that contains the query string parameters from the request that was captured. - `add_passthru` now supports `re` pattern objects to match URLs. - ConnectionErrors raised by responses now include more details on the request that was attempted and the mocks registered. 0.10.9 ------ - Fixed regression with `add_callback()` and content-type header. - Fixed implicit dependency on urllib3>1.23.0 0.10.8 ------ - Fixed cookie parsing and enabled multiple cookies to be set by using a list of tuple values. 0.10.7 ------ - Added pypi badges to README. - Fixed formatting issues in README. - Quoted cookie values are returned correctly now. - Improved compatibility for pytest 5 - Module level method names are no longer generated dynamically improving IDE navigation. 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) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1642535104.0 responses-0.18.0/LICENSE0000644000175100001710000002512300000000000014171 0ustar00runnerdocker 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. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1642535104.0 responses-0.18.0/MANIFEST.in0000644000175100001710000000022400000000000014715 0ustar00runnerdockerinclude README.rst CHANGES LICENSE include test_responses.py test_matchers.py test_registries.py include **/*.pyi include tox.ini global-exclude *~ ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1642535106.8147693 responses-0.18.0/PKG-INFO0000644000175100001710000007062600000000000014271 0ustar00runnerdockerMetadata-Version: 2.1 Name: responses Version: 0.18.0 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 :: 3 Classifier: Programming Language :: Python :: 3.7 Classifier: Programming Language :: Python :: 3.8 Classifier: Programming Language :: Python :: 3.9 Classifier: Programming Language :: Python :: 3.10 Classifier: Topic :: Software Development Requires-Python: >=3.7 Description-Content-Type: text/x-rst Provides-Extra: tests License-File: LICENSE Responses ========= .. image:: https://img.shields.io/pypi/v/responses.svg :target: https://pypi.python.org/pypi/responses/ .. image:: https://img.shields.io/pypi/pyversions/responses.svg :target: https://pypi.org/project/responses/ .. image:: https://codecov.io/gh/getsentry/responses/branch/master/graph/badge.svg :target: https://codecov.io/gh/getsentry/responses/ A utility library for mocking out the ``requests`` Python library. .. note:: Responses requires Python 3.7 or newer, and requests >= 2.0 Table of Contents ----------------- .. contents:: 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``) DEPRECATED: Use ``responses.matchers.query_param_matcher`` or ``responses.matchers.query_string_matcher`` 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``) DEPRECATED: use ``stream`` argument in request directly auto_calculate_content_length (``bool``) Disabled by default. Automatically calculates the length of a supplied string or JSON body. match (``list``) A list of callbacks to match requests based on request attributes. Current module provides multiple matchers that you can use to match: * body contents in JSON format * body contents in URL encoded data format * request query parameters * request query string (similar to query parameters but takes string as input) * kwargs provided to request e.g. ``stream``, ``verify`` * 'multipart/form-data' content and headers in request * request headers * request fragment identifier Alternatively user can create custom matcher. Read more `Matching Requests`_ Matching Requests ----------------- Matching Request Body Contents ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ When adding responses for endpoints that are sent request data you can add matchers to ensure your code is sending the right parameters and provide different responses based on the request body contents. ``responses`` provides matchers for JSON and URL-encoded request bodies. URL-encoded data """""""""""""""" .. code-block:: python import responses import requests from responses import matchers @responses.activate def test_calc_api(): responses.add( responses.POST, url='http://calc.com/sum', body="4", match=[ matchers.urlencoded_params_matcher({"left": "1", "right": "3"}) ] ) requests.post("http://calc.com/sum", data={"left": 1, "right": 3}) JSON encoded data """"""""""""""""" Matching JSON encoded data can be done with ``matchers.json_params_matcher()``. .. code-block:: python import responses import requests from responses import matchers @responses.activate def test_calc_api(): responses.add( method=responses.POST, url="http://example.com/", body="one", match=[matchers.json_params_matcher({"page": {"name": "first", "type": "json"}})], ) resp = requests.request( "POST", "http://example.com/", headers={"Content-Type": "application/json"}, json={"page": {"name": "first", "type": "json"}}, ) Query Parameters Matcher ^^^^^^^^^^^^^^^^^^^^^^^^ Query Parameters as a Dictionary """""""""""""""""""""""""""""""" You can use the ``matchers.query_param_matcher`` function to match against the ``params`` request parameter. Just use the same dictionary as you will use in ``params`` argument in ``request``. Note, do not use query parameters as part of the URL. Avoid using ``match_querystring`` deprecated argument. .. code-block:: python import responses import requests from responses import matchers @responses.activate def test_calc_api(): url = "http://example.com/test" params = {"hello": "world", "I am": "a big test"} responses.add( method=responses.GET, url=url, body="test", match=[matchers.query_param_matcher(params)], match_querystring=False, ) resp = requests.get(url, params=params) constructed_url = r"http://example.com/test?I+am=a+big+test&hello=world" assert resp.url == constructed_url assert resp.request.url == constructed_url assert resp.request.params == params Query Parameters as a String """""""""""""""""""""""""""" As alternative, you can use query string value in ``matchers.query_string_matcher`` to match query parameters in your request .. code-block:: python import requests import responses from responses import matchers @responses.activate def my_func(): responses.add( responses.GET, "https://httpbin.org/get", match=[matchers.query_string_matcher("didi=pro&test=1")], ) resp = requests.get("https://httpbin.org/get", params={"test": 1, "didi": "pro"}) my_func() Request Keyword Arguments Matcher ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ To validate request arguments use the ``matchers.request_kwargs_matcher`` function to match against the request kwargs. Note, only arguments provided to ``matchers.request_kwargs_matcher`` will be validated. .. code-block:: python import responses import requests from responses import matchers with responses.RequestsMock(assert_all_requests_are_fired=False) as rsps: req_kwargs = { "stream": True, "verify": False, } rsps.add( "GET", "http://111.com", match=[matchers.request_kwargs_matcher(req_kwargs)], ) requests.get("http://111.com", stream=True) # >>> Arguments don't match: {stream: True, verify: True} doesn't match {stream: True, verify: False} Request multipart/form-data Data Validation ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ To validate request body and headers for ``multipart/form-data`` data you can use ``matchers.multipart_matcher``. The ``data``, and ``files`` parameters provided will be compared to the request: .. code-block:: python import requests import responses from responses.matchers import multipart_matcher @responses.activate def my_func(): req_data = {"some": "other", "data": "fields"} req_files = {"file_name": b"Old World!"} responses.add( responses.POST, url="http://httpbin.org/post", match=[multipart_matcher(req_files, data=req_data)] ) resp = requests.post("http://httpbin.org/post", files={"file_name": b"New World!"}) my_func() # >>> raises ConnectionError: multipart/form-data doesn't match. Request body differs. Request Fragment Identifier Validation ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ To validate request URL fragment identifier you can use ``matchers.fragment_identifier_matcher``. The matcher takes fragment string (everything after ``#`` sign) as input for comparison: .. code-block:: python import requests import responses from responses.matchers import fragment_identifier_matcher @responses.activate def run(): url = "http://example.com?ab=xy&zed=qwe#test=1&foo=bar" responses.add( responses.GET, url, match_querystring=True, match=[fragment_identifier_matcher("test=1&foo=bar")], body=b"test", ) # two requests to check reversed order of fragment identifier resp = requests.get("http://example.com?ab=xy&zed=qwe#test=1&foo=bar") resp = requests.get("http://example.com?zed=qwe&ab=xy#foo=bar&test=1") run() Request Headers Validation ^^^^^^^^^^^^^^^^^^^^^^^^^^ When adding responses you can specify matchers to ensure that your code is sending the right headers and provide different responses based on the request headers. .. code-block:: python import responses import requests from responses import matchers @responses.activate def test_content_type(): responses.add( responses.GET, url="http://example.com/", body="hello world", match=[ matchers.header_matcher({"Accept": "text/plain"}) ] ) responses.add( responses.GET, url="http://example.com/", json={"content": "hello world"}, match=[ matchers.header_matcher({"Accept": "application/json"}) ] ) # request in reverse order to how they were added! resp = requests.get("http://example.com/", headers={"Accept": "application/json"}) assert resp.json() == {"content": "hello world"} resp = requests.get("http://example.com/", headers={"Accept": "text/plain"}) assert resp.text == "hello world" Because ``requests`` will send several standard headers in addition to what was specified by your code, request headers that are additional to the ones passed to the matcher are ignored by default. You can change this behaviour by passing ``strict_match=True`` to the matcher to ensure that only the headers that you're expecting are sent and no others. Note that you will probably have to use a ``PreparedRequest`` in your code to ensure that ``requests`` doesn't include any additional headers. .. code-block:: python import responses import requests from responses import matchers @responses.activate def test_content_type(): responses.add( responses.GET, url="http://example.com/", body="hello world", match=[ matchers.header_matcher({"Accept": "text/plain"}, strict_match=True) ] ) # this will fail because requests adds its own headers with pytest.raises(ConnectionError): requests.get("http://example.com/", headers={"Accept": "text/plain"}) # a prepared request where you overwrite the headers before sending will work session = requests.Session() prepped = session.prepare_request( requests.Request( method="GET", url="http://example.com/", ) ) prepped.headers = {"Accept": "text/plain"} resp = session.send(prepped) assert resp.text == "hello world" Creating Custom Matcher ^^^^^^^^^^^^^^^^^^^^^^^ If your application requires other encodings or different data validation you can build your own matcher that returns ``Tuple[matches: bool, reason: str]``. Where boolean represents ``True`` or ``False`` if the request parameters match and the string is a reason in case of match failure. Your matcher can expect a ``PreparedRequest`` parameter to be provided by ``responses``. Note, ``PreparedRequest`` is customized and has additional attributes ``params`` and ``req_kwargs``. Response Registry --------------------------- By default, ``responses`` will search all registered ``Response`` objects and return a match. If only one ``Response`` is registered, the registry is kept unchanged. However, if multiple matches are found for the same request, then first match is returned and removed from registry. Such behavior is suitable for most of use cases, but to handle special conditions, you can implement custom registry which must follow interface of ``registries.FirstMatchRegistry``. Redefining the ``find`` method will allow you to create custom search logic and return appropriate ``Response`` Example that shows how to set custom registry .. code-block:: python import responses from responses import registries class CustomRegistry(registries.FirstMatchRegistry): pass """ Before tests: """ # using function decorator @responses.activate(registry=CustomRegistry) def run(): """ Within test: <__main__.CustomRegistry object> """ run() """ After test: """ # using context manager with responses.RequestsMock(registry=CustomRegistry) as rsps: """ In context manager: <__main__.CustomRegistry object> """ """ After exit from context manager: """ 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', ) You can see params passed in the original ``request`` in ``responses.calls[].request.params``: .. code-block:: python import responses import requests @responses.activate def test_request_params(): responses.add( method=responses.GET, url="http://example.com?hello=world", body="test", match_querystring=False, ) resp = requests.get('http://example.com', params={"hello": "world"}) assert responses.calls[0].request.params == {"hello": "world"} 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 Responses inside a unittest setUp() ----------------------------------- When run with unittest tests, this can be used to set up some generic class-level responses, that may be complemented by each test .. code-block:: python class TestMyApi(unittest.TestCase): def setUp(self): responses.add(responses.GET, 'https://example.com', body="within setup") # here go other self.responses.add(...) @responses.activate def test_my_func(self): responses.add( responses.GET, "https://httpbin.org/get", match=[matchers.query_param_matcher({"test": "1", "didi": "pro"})], body="within test" ) resp = requests.get("https://example.com") resp2 = requests.get("https://httpbin.org/get", params={"test": "1", "didi": "pro"}) print(resp.text) # >>> within setup print(resp2.text) # >>> within test 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') assert_call_count ----------------- Assert that the request was called exactly n times. .. code-block:: python import responses import requests @responses.activate def test_assert_call_count(): responses.add(responses.GET, "http://example.com") requests.get("http://example.com") assert responses.assert_call_count("http://example.com", 1) is True requests.get("http://example.com") with pytest.raises(AssertionError) as excinfo: responses.assert_call_count("http://example.com", 1) assert "Expected URL 'http://example.com' to be called 1 times. Called 2 times." in str(excinfo.value) 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 through real requests ----------------------------- In some cases you may wish to allow for certain requests to pass through responses and hit a real server. This can be done with the ``add_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. Pass through endpoints can be configured with regex patterns if you need to allow an entire domain or path subtree to send requests: .. code-block:: python responses.add_passthru(re.compile('https://percy.io/\\w+')) Lastly, you can use the `response.passthrough` attribute on `BaseResponse` or use ``PassthroughResponse`` to enable a response to behave as a pass through. .. code-block:: python # Enable passthrough for a single response response = Response(responses.GET, 'http://example.com', body='not used') response.passthrough = True responses.add(response) # Use PassthroughResponse response = PassthroughResponse(responses.GET, 'http://example.com') responses.add(response) Viewing/Modifying registered responses -------------------------------------- Registered responses are available as a public method of the RequestMock instance. It is sometimes useful for debugging purposes to view the stack of registered responses which can be accessed via ``responses.registered()``. 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} The ``upsert`` function allows a previously registered ``response`` to be changed like ``replace``. If the response is registered, the ``upsert`` function will registered it like ``add``. ``remove`` takes a ``method`` and ``url`` argument and will remove **all** matched responses from the registered list. Finally, ``reset`` will reset all registered responses. Contributing ------------ Environment Configuration ^^^^^^^^^^^^^^^^^^^^^^^^^ 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 Tests and Code Quality Validation ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ The easiest way to validate your code is to run tests via ``tox``. Current ``tox`` configuration runs the same checks that are used in GitHub Actions CI/CD pipeline. Please execute the following command line from the project root to validate your code against: * Unit tests in all Python versions that are supported by this project * Type validation via ``mypy`` * All ``pre-commit`` hooks .. code-block:: shell tox Alternatively, you can always run a single test. See documentation below. Unit tests """""""""" Responses uses `Pytest `_ for testing. You can run all tests by: .. code-block:: shell tox -e py37 tox -e py310 OR manually activate required version of Python and run .. code-block:: shell pytest And run a single test by: .. code-block:: shell pytest -k '' Type Validation """"""""""""""" To verify ``type`` compliance, run `mypy `_ linter: .. code-block:: shell tox -e mypy OR .. code-block:: shell mypy --config-file=./mypy.ini -p responses Code Quality and Style """""""""""""""""""""" To check code style and reformat it run: .. code-block:: shell tox -e precom OR .. code-block:: shell pre-commit run --all-files Note: on some OS, you have to use ``pre_commit`` ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1642535104.0 responses-0.18.0/README.rst0000644000175100001710000006713600000000000014665 0ustar00runnerdockerResponses ========= .. image:: https://img.shields.io/pypi/v/responses.svg :target: https://pypi.python.org/pypi/responses/ .. image:: https://img.shields.io/pypi/pyversions/responses.svg :target: https://pypi.org/project/responses/ .. image:: https://codecov.io/gh/getsentry/responses/branch/master/graph/badge.svg :target: https://codecov.io/gh/getsentry/responses/ A utility library for mocking out the ``requests`` Python library. .. note:: Responses requires Python 3.7 or newer, and requests >= 2.0 Table of Contents ----------------- .. contents:: 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``) DEPRECATED: Use ``responses.matchers.query_param_matcher`` or ``responses.matchers.query_string_matcher`` 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``) DEPRECATED: use ``stream`` argument in request directly auto_calculate_content_length (``bool``) Disabled by default. Automatically calculates the length of a supplied string or JSON body. match (``list``) A list of callbacks to match requests based on request attributes. Current module provides multiple matchers that you can use to match: * body contents in JSON format * body contents in URL encoded data format * request query parameters * request query string (similar to query parameters but takes string as input) * kwargs provided to request e.g. ``stream``, ``verify`` * 'multipart/form-data' content and headers in request * request headers * request fragment identifier Alternatively user can create custom matcher. Read more `Matching Requests`_ Matching Requests ----------------- Matching Request Body Contents ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ When adding responses for endpoints that are sent request data you can add matchers to ensure your code is sending the right parameters and provide different responses based on the request body contents. ``responses`` provides matchers for JSON and URL-encoded request bodies. URL-encoded data """""""""""""""" .. code-block:: python import responses import requests from responses import matchers @responses.activate def test_calc_api(): responses.add( responses.POST, url='http://calc.com/sum', body="4", match=[ matchers.urlencoded_params_matcher({"left": "1", "right": "3"}) ] ) requests.post("http://calc.com/sum", data={"left": 1, "right": 3}) JSON encoded data """"""""""""""""" Matching JSON encoded data can be done with ``matchers.json_params_matcher()``. .. code-block:: python import responses import requests from responses import matchers @responses.activate def test_calc_api(): responses.add( method=responses.POST, url="http://example.com/", body="one", match=[matchers.json_params_matcher({"page": {"name": "first", "type": "json"}})], ) resp = requests.request( "POST", "http://example.com/", headers={"Content-Type": "application/json"}, json={"page": {"name": "first", "type": "json"}}, ) Query Parameters Matcher ^^^^^^^^^^^^^^^^^^^^^^^^ Query Parameters as a Dictionary """""""""""""""""""""""""""""""" You can use the ``matchers.query_param_matcher`` function to match against the ``params`` request parameter. Just use the same dictionary as you will use in ``params`` argument in ``request``. Note, do not use query parameters as part of the URL. Avoid using ``match_querystring`` deprecated argument. .. code-block:: python import responses import requests from responses import matchers @responses.activate def test_calc_api(): url = "http://example.com/test" params = {"hello": "world", "I am": "a big test"} responses.add( method=responses.GET, url=url, body="test", match=[matchers.query_param_matcher(params)], match_querystring=False, ) resp = requests.get(url, params=params) constructed_url = r"http://example.com/test?I+am=a+big+test&hello=world" assert resp.url == constructed_url assert resp.request.url == constructed_url assert resp.request.params == params Query Parameters as a String """""""""""""""""""""""""""" As alternative, you can use query string value in ``matchers.query_string_matcher`` to match query parameters in your request .. code-block:: python import requests import responses from responses import matchers @responses.activate def my_func(): responses.add( responses.GET, "https://httpbin.org/get", match=[matchers.query_string_matcher("didi=pro&test=1")], ) resp = requests.get("https://httpbin.org/get", params={"test": 1, "didi": "pro"}) my_func() Request Keyword Arguments Matcher ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ To validate request arguments use the ``matchers.request_kwargs_matcher`` function to match against the request kwargs. Note, only arguments provided to ``matchers.request_kwargs_matcher`` will be validated. .. code-block:: python import responses import requests from responses import matchers with responses.RequestsMock(assert_all_requests_are_fired=False) as rsps: req_kwargs = { "stream": True, "verify": False, } rsps.add( "GET", "http://111.com", match=[matchers.request_kwargs_matcher(req_kwargs)], ) requests.get("http://111.com", stream=True) # >>> Arguments don't match: {stream: True, verify: True} doesn't match {stream: True, verify: False} Request multipart/form-data Data Validation ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ To validate request body and headers for ``multipart/form-data`` data you can use ``matchers.multipart_matcher``. The ``data``, and ``files`` parameters provided will be compared to the request: .. code-block:: python import requests import responses from responses.matchers import multipart_matcher @responses.activate def my_func(): req_data = {"some": "other", "data": "fields"} req_files = {"file_name": b"Old World!"} responses.add( responses.POST, url="http://httpbin.org/post", match=[multipart_matcher(req_files, data=req_data)] ) resp = requests.post("http://httpbin.org/post", files={"file_name": b"New World!"}) my_func() # >>> raises ConnectionError: multipart/form-data doesn't match. Request body differs. Request Fragment Identifier Validation ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ To validate request URL fragment identifier you can use ``matchers.fragment_identifier_matcher``. The matcher takes fragment string (everything after ``#`` sign) as input for comparison: .. code-block:: python import requests import responses from responses.matchers import fragment_identifier_matcher @responses.activate def run(): url = "http://example.com?ab=xy&zed=qwe#test=1&foo=bar" responses.add( responses.GET, url, match_querystring=True, match=[fragment_identifier_matcher("test=1&foo=bar")], body=b"test", ) # two requests to check reversed order of fragment identifier resp = requests.get("http://example.com?ab=xy&zed=qwe#test=1&foo=bar") resp = requests.get("http://example.com?zed=qwe&ab=xy#foo=bar&test=1") run() Request Headers Validation ^^^^^^^^^^^^^^^^^^^^^^^^^^ When adding responses you can specify matchers to ensure that your code is sending the right headers and provide different responses based on the request headers. .. code-block:: python import responses import requests from responses import matchers @responses.activate def test_content_type(): responses.add( responses.GET, url="http://example.com/", body="hello world", match=[ matchers.header_matcher({"Accept": "text/plain"}) ] ) responses.add( responses.GET, url="http://example.com/", json={"content": "hello world"}, match=[ matchers.header_matcher({"Accept": "application/json"}) ] ) # request in reverse order to how they were added! resp = requests.get("http://example.com/", headers={"Accept": "application/json"}) assert resp.json() == {"content": "hello world"} resp = requests.get("http://example.com/", headers={"Accept": "text/plain"}) assert resp.text == "hello world" Because ``requests`` will send several standard headers in addition to what was specified by your code, request headers that are additional to the ones passed to the matcher are ignored by default. You can change this behaviour by passing ``strict_match=True`` to the matcher to ensure that only the headers that you're expecting are sent and no others. Note that you will probably have to use a ``PreparedRequest`` in your code to ensure that ``requests`` doesn't include any additional headers. .. code-block:: python import responses import requests from responses import matchers @responses.activate def test_content_type(): responses.add( responses.GET, url="http://example.com/", body="hello world", match=[ matchers.header_matcher({"Accept": "text/plain"}, strict_match=True) ] ) # this will fail because requests adds its own headers with pytest.raises(ConnectionError): requests.get("http://example.com/", headers={"Accept": "text/plain"}) # a prepared request where you overwrite the headers before sending will work session = requests.Session() prepped = session.prepare_request( requests.Request( method="GET", url="http://example.com/", ) ) prepped.headers = {"Accept": "text/plain"} resp = session.send(prepped) assert resp.text == "hello world" Creating Custom Matcher ^^^^^^^^^^^^^^^^^^^^^^^ If your application requires other encodings or different data validation you can build your own matcher that returns ``Tuple[matches: bool, reason: str]``. Where boolean represents ``True`` or ``False`` if the request parameters match and the string is a reason in case of match failure. Your matcher can expect a ``PreparedRequest`` parameter to be provided by ``responses``. Note, ``PreparedRequest`` is customized and has additional attributes ``params`` and ``req_kwargs``. Response Registry --------------------------- By default, ``responses`` will search all registered ``Response`` objects and return a match. If only one ``Response`` is registered, the registry is kept unchanged. However, if multiple matches are found for the same request, then first match is returned and removed from registry. Such behavior is suitable for most of use cases, but to handle special conditions, you can implement custom registry which must follow interface of ``registries.FirstMatchRegistry``. Redefining the ``find`` method will allow you to create custom search logic and return appropriate ``Response`` Example that shows how to set custom registry .. code-block:: python import responses from responses import registries class CustomRegistry(registries.FirstMatchRegistry): pass """ Before tests: """ # using function decorator @responses.activate(registry=CustomRegistry) def run(): """ Within test: <__main__.CustomRegistry object> """ run() """ After test: """ # using context manager with responses.RequestsMock(registry=CustomRegistry) as rsps: """ In context manager: <__main__.CustomRegistry object> """ """ After exit from context manager: """ 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', ) You can see params passed in the original ``request`` in ``responses.calls[].request.params``: .. code-block:: python import responses import requests @responses.activate def test_request_params(): responses.add( method=responses.GET, url="http://example.com?hello=world", body="test", match_querystring=False, ) resp = requests.get('http://example.com', params={"hello": "world"}) assert responses.calls[0].request.params == {"hello": "world"} 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 Responses inside a unittest setUp() ----------------------------------- When run with unittest tests, this can be used to set up some generic class-level responses, that may be complemented by each test .. code-block:: python class TestMyApi(unittest.TestCase): def setUp(self): responses.add(responses.GET, 'https://example.com', body="within setup") # here go other self.responses.add(...) @responses.activate def test_my_func(self): responses.add( responses.GET, "https://httpbin.org/get", match=[matchers.query_param_matcher({"test": "1", "didi": "pro"})], body="within test" ) resp = requests.get("https://example.com") resp2 = requests.get("https://httpbin.org/get", params={"test": "1", "didi": "pro"}) print(resp.text) # >>> within setup print(resp2.text) # >>> within test 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') assert_call_count ----------------- Assert that the request was called exactly n times. .. code-block:: python import responses import requests @responses.activate def test_assert_call_count(): responses.add(responses.GET, "http://example.com") requests.get("http://example.com") assert responses.assert_call_count("http://example.com", 1) is True requests.get("http://example.com") with pytest.raises(AssertionError) as excinfo: responses.assert_call_count("http://example.com", 1) assert "Expected URL 'http://example.com' to be called 1 times. Called 2 times." in str(excinfo.value) 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 through real requests ----------------------------- In some cases you may wish to allow for certain requests to pass through responses and hit a real server. This can be done with the ``add_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. Pass through endpoints can be configured with regex patterns if you need to allow an entire domain or path subtree to send requests: .. code-block:: python responses.add_passthru(re.compile('https://percy.io/\\w+')) Lastly, you can use the `response.passthrough` attribute on `BaseResponse` or use ``PassthroughResponse`` to enable a response to behave as a pass through. .. code-block:: python # Enable passthrough for a single response response = Response(responses.GET, 'http://example.com', body='not used') response.passthrough = True responses.add(response) # Use PassthroughResponse response = PassthroughResponse(responses.GET, 'http://example.com') responses.add(response) Viewing/Modifying registered responses -------------------------------------- Registered responses are available as a public method of the RequestMock instance. It is sometimes useful for debugging purposes to view the stack of registered responses which can be accessed via ``responses.registered()``. 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} The ``upsert`` function allows a previously registered ``response`` to be changed like ``replace``. If the response is registered, the ``upsert`` function will registered it like ``add``. ``remove`` takes a ``method`` and ``url`` argument and will remove **all** matched responses from the registered list. Finally, ``reset`` will reset all registered responses. Contributing ------------ Environment Configuration ^^^^^^^^^^^^^^^^^^^^^^^^^ 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 Tests and Code Quality Validation ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ The easiest way to validate your code is to run tests via ``tox``. Current ``tox`` configuration runs the same checks that are used in GitHub Actions CI/CD pipeline. Please execute the following command line from the project root to validate your code against: * Unit tests in all Python versions that are supported by this project * Type validation via ``mypy`` * All ``pre-commit`` hooks .. code-block:: shell tox Alternatively, you can always run a single test. See documentation below. Unit tests """""""""" Responses uses `Pytest `_ for testing. You can run all tests by: .. code-block:: shell tox -e py37 tox -e py310 OR manually activate required version of Python and run .. code-block:: shell pytest And run a single test by: .. code-block:: shell pytest -k '' Type Validation """"""""""""""" To verify ``type`` compliance, run `mypy `_ linter: .. code-block:: shell tox -e mypy OR .. code-block:: shell mypy --config-file=./mypy.ini -p responses Code Quality and Style """""""""""""""""""""" To check code style and reformat it run: .. code-block:: shell tox -e precom OR .. code-block:: shell pre-commit run --all-files Note: on some OS, you have to use ``pre_commit`` ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1642535106.8147693 responses-0.18.0/responses/0000755000175100001710000000000000000000000015202 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1642535104.0 responses-0.18.0/responses/__init__.py0000644000175100001710000006242600000000000017325 0ustar00runnerdockerfrom __future__ import absolute_import, print_function, division, unicode_literals import _io from http import client from http import cookies import json as json_module import logging import re from itertools import groupby from collections import namedtuple from functools import wraps from requests.adapters import HTTPAdapter from requests.exceptions import ConnectionError from requests.utils import cookiejar_from_dict from responses.matchers import json_params_matcher as _json_params_matcher from responses.matchers import urlencoded_params_matcher as _urlencoded_params_matcher from responses.registries import FirstMatchRegistry from responses.matchers import query_string_matcher as _query_string_matcher from warnings import warn from collections.abc import Sequence, Sized try: from requests.packages.urllib3.response import HTTPResponse except ImportError: # pragma: no cover from urllib3.response import HTTPResponse # pragma: no cover try: from requests.packages.urllib3.connection import HTTPHeaderDict except ImportError: # pragma: no cover from urllib3.response import HTTPHeaderDict # pragma: no cover try: from requests.packages.urllib3.util.url import parse_url except ImportError: # pragma: no cover from urllib3.util.url import parse_url # pragma: no cover from urllib.parse import ( urlparse, urlunparse, parse_qsl, urlsplit, urlunsplit, quote, ) from io import BytesIO as BufferIO from unittest import mock as std_mock Pattern = re.Pattern UNSET = object() Call = namedtuple("Call", ["request", "response"]) _real_send = HTTPAdapter.send logger = logging.getLogger("responses") class FalseBool: # used for backwards compatibility, see # https://github.com/getsentry/responses/issues/464 def __bool__(self): return False __nonzero__ = __bool__ def urlencoded_params_matcher(params): warn( "Function is deprecated. Use 'from responses.matchers import urlencoded_params_matcher'", DeprecationWarning, ) return _urlencoded_params_matcher(params) def json_params_matcher(params): warn( "Function is deprecated. Use 'from responses.matchers import json_params_matcher'", DeprecationWarning, ) return _json_params_matcher(params) 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 chars = list(url) for i, x in enumerate(chars): if ord(x) > 128: chars[i] = quote(x) return "".join(chars) def _cookies_from_headers(headers): resp_cookie = cookies.SimpleCookie() resp_cookie.load(headers["set-cookie"]) cookies_dict = {name: v.value for name, v in resp_cookie.items()} return cookiejar_from_dict(cookies_dict) def get_wrapped(func, responses, registry=None): if registry is not None: responses._set_registry(registry) @wraps(func) def wrapper(*args, **kwargs): with responses: return func(*args, **kwargs) 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 isinstance(url, str): url_parts = list(urlsplit(url)) if url_parts[2] == "": url_parts[2] = "/" url = urlunsplit(url_parts) return url def _get_url_and_path(url): url_parsed = urlparse(url) url_and_path = urlunparse( [url_parsed.scheme, url_parsed.netloc, url_parsed.path, None, None, None] ) return parse_url(url_and_path).url def _handle_body(body): if isinstance(body, str): body = body.encode("utf-8") if isinstance(body, _io.BufferedReader): return body data = BufferIO(body) def is_closed(): """ Real Response uses HTTPResponse as body object. Thus, when method is_closed is called first to check if there is any more content to consume and the file-like object is still opened This method ensures stability to work for both: https://github.com/getsentry/responses/issues/438 https://github.com/getsentry/responses/issues/394 where file should be intentionally be left opened to continue consumption """ if not data.closed and data.read(1): # if there is more bytes to read then keep open, but return pointer data.seek(-1, 1) return False else: if not data.closed: # close but return False to mock like is still opened data.close() return False # only if file really closed (by us) return True return True data.isclosed = is_closed return data class BaseResponse(object): passthrough = False content_type = None headers = None stream = False def __init__(self, method, url, match_querystring=None, match=()): self.method = method # ensure the url has a default path set if the url is a string self.url = _ensure_url_default_path(url) if self._should_match_querystring(match_querystring): match = tuple(match) + (_query_string_matcher(urlparse(self.url).query),) self.match = match 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 an 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 _should_match_querystring(self, match_querystring_argument): if isinstance(self.url, Pattern): # the old default from <= 0.9.0 return False if match_querystring_argument is not None: if not isinstance(match_querystring_argument, FalseBool): warn( ( "Argument 'match_querystring' is deprecated. " "Use 'responses.matchers.query_param_matcher' or " "'responses.matchers.query_string_matcher'" ), DeprecationWarning, ) return match_querystring_argument return bool(urlparse(self.url).query) def _url_matches(self, url, other): if isinstance(url, str): if _has_unicode(url): url = _clean_unicode(url) return _get_url_and_path(url) == _get_url_and_path(other) elif isinstance(url, Pattern) and url.match(other): return True else: return False @staticmethod def _req_attr_matches(match, request): for matcher in match: valid, reason = matcher(request) if not valid: return False, reason return True, "" def get_headers(self): headers = HTTPHeaderDict() # Duplicate headers are legal if self.content_type is not None: headers["Content-Type"] = self.content_type if self.headers: headers.extend(self.headers) return headers def get_response(self, request): raise NotImplementedError def matches(self, request): if request.method != self.method: return False, "Method does not match" if not self._url_matches(self.url, request.url): return False, "URL does not match" valid, reason = self._req_attr_matches(self.match, request) if not valid: return False, reason return True, "" class Response(BaseResponse): def __init__( self, method, url, body="", json=None, status=200, headers=None, stream=None, content_type=UNSET, auto_calculate_content_length=False, **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: if isinstance(body, str) and _has_unicode(body): content_type = "text/plain; charset=utf-8" else: content_type = "text/plain" self.body = body self.status = status self.headers = headers if stream is not None: warn( "stream argument is deprecated. Use stream parameter in request directly", DeprecationWarning, ) self.stream = stream self.content_type = content_type self.auto_calculate_content_length = auto_calculate_content_length 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) if ( self.auto_calculate_content_length and isinstance(body, BufferIO) and "Content-Length" not in headers ): content_length = len(body.getvalue()) headers["Content-Length"] = str(content_length) return HTTPResponse( status=status, reason=client.responses.get(status, None), body=body, headers=headers, original_response=OriginalResponseShim(headers), preload_content=False, ) def __repr__(self): return ( "".format( url=self.url, status=self.status, content_type=self.content_type, headers=json_module.dumps(self.headers), ) ) class CallbackResponse(BaseResponse): def __init__( self, method, url, callback, stream=None, content_type="text/plain", **kwargs ): self.callback = callback if stream is not None: warn( "stream argument is deprecated. Use stream parameter in request directly", DeprecationWarning, ) 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 # If the callback set a content-type remove the one # set in add_callback() so that we don't have multiple # content type values. has_content_type = False if isinstance(r_headers, dict) and "Content-Type" in r_headers: has_content_type = True elif isinstance(r_headers, list): has_content_type = any( [h for h in r_headers if h and h[0].lower() == "content-type"] ) if has_content_type: headers.pop("Content-Type", None) body = _handle_body(body) headers.extend(r_headers) return HTTPResponse( status=status, reason=client.responses.get(status, None), body=body, headers=headers, original_response=OriginalResponseShim(headers), preload_content=False, ) class PassthroughResponse(BaseResponse): passthrough = True class OriginalResponseShim(object): """ Shim for compatibility with older versions of urllib3 requests cookie handling depends on responses having a property chain of `response._original_response.msg` which contains the response headers [1] Using HTTPResponse() for this purpose causes compatibility errors with urllib3<1.23.0. To avoid adding more dependencies we can use this shim. [1]: https://github.com/psf/requests/blob/75bdc998e2d/requests/cookies.py#L125 """ def __init__(self, headers): self.msg = headers def isclosed(self): return True def close(self): return 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", registry=FirstMatchRegistry, ): self._calls = CallList() self.reset() self._registry = registry() # call only after 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 self._patcher = None def _get_registry(self): return self._registry def _set_registry(self, new_registry): if self.registered(): err_msg = ( "Cannot replace Registry, current registry has responses.\n" "Run 'responses.registry.reset()' first" ) raise AttributeError(err_msg) self._registry = new_registry() def reset(self): self._registry = FirstMatchRegistry() self._calls.reset() self.passthru_prefixes = () def add( self, method=None, # method or ``Response`` url=None, body="", adding_headers=None, *args, **kwargs ): """ >>> import responses 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'}, >>> ) """ if isinstance(method, BaseResponse): self._registry.add(method) return if adding_headers is not None: kwargs.setdefault("headers", adding_headers) self._registry.add(Response(method=method, url=url, body=body, **kwargs)) def add_passthru(self, prefix): """ Register a URL prefix or regex 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: >>> import responses >>> responses.add_passthru('https://example.com') Regex can be used like: >>> responses.add_passthru(re.compile('https://example.com/\\w+')) """ if not isinstance(prefix, Pattern) and _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. >>> import responses >>> responses.add(responses.GET, 'http://example.org') >>> responses.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) self._registry.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. >>> import responses >>> 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): url = method_or_response.url response = method_or_response else: response = Response(method=method_or_response, url=url, body=body, **kwargs) self._registry.replace(response) def upsert(self, method_or_response=None, url=None, body="", *args, **kwargs): """ Replaces a response previously added using ``add()``, or adds the response if no response exists. Responses are matched using ``method``and ``url``. The first matching response is replaced. >>> import responses >>> responses.add(responses.GET, 'http://example.org', json={'data': 1}) >>> responses.upsert(responses.GET, 'http://example.org', json={'data': 2}) """ try: self.replace(method_or_response, url, body, *args, **kwargs) except ValueError: self.add(method_or_response, url, body, *args, **kwargs) def add_callback( self, method, url, callback, match_querystring=FalseBool(), content_type="text/plain", match=(), ): # ensure the url has a default path set if the url is a string # url = _ensure_url_default_path(url, match_querystring) self._registry.add( CallbackResponse( url=url, method=method, callback=callback, content_type=content_type, match_querystring=match_querystring, match=match, ) ) def registered(self): return self._registry.registered @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=None, registry=None): if func is not None: return get_wrapped(func, self) def deco_activate(func): return get_wrapped(func, self, registry) return deco_activate def _find_match(self, request): """ Iterates through all available matches and validates if any of them matches the request :param request: (PreparedRequest), request object :return: (Response) found match. If multiple found, then remove & return the first match. (list) list with reasons why other matches don't match """ return self._registry.find(request) def _parse_request_params(self, url): params = {} for key, val in groupby(parse_qsl(urlparse(url).query), lambda kv: kv[0]): values = list(map(lambda x: x[1], val)) if len(values) == 1: values = values[0] params[key] = values return params def _on_request(self, adapter, request, **kwargs): # add attributes params and req_kwargs to 'request' object for further match comparison # original request object does not have these attributes request.params = self._parse_request_params(request.path_url) request.req_kwargs = kwargs match, match_failed_reasons = self._find_match(request) resp_callback = self.response_callback if match is None: if any( [ p.match(request.url) if isinstance(p, Pattern) else request.url.startswith(p) for p in self.passthru_prefixes ] ): logger.info("request.allowed-passthru", extra={"url": request.url}) return _real_send(adapter, request, **kwargs) error_msg = ( "Connection refused by Responses - the call doesn't " "match any registered mock.\n\n" "Request: \n" "- %s %s\n\n" "Available matches:\n" % (request.method, request.url) ) for i, m in enumerate(self.registered()): error_msg += "- {} {} {}\n".format( m.method, m.url, match_failed_reasons[i] ) response = ConnectionError(error_msg) response.request = request self._calls.add(request, response) response = resp_callback(response) if resp_callback else response raise response if match.passthrough: logger.info("request.passthrough-response", extra={"url": request.url}) response = _real_send(adapter, request, **kwargs) else: try: response = adapter.build_response(request, match.get_response(request)) except BaseException as response: match.call_count += 1 self._calls.add(request, response) response = resp_callback(response) if resp_callback else response raise 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.registered() 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] ) ) def assert_call_count(self, url, count): call_count = len( [ 1 for call in self.calls if call.request.url == _ensure_url_default_path(url) ] ) if call_count == count: return True else: raise AssertionError( "Expected URL '{0}' to be called {1} times. Called {2} times.".format( url, count, call_count ) ) # expose default mock namespace mock = _default_mock = RequestsMock(assert_all_requests_are_fired=False) __all__ = [ "CallbackResponse", "Response", "RequestsMock", # Exposed by the RequestsMock class: "activate", "add", "add_callback", "add_passthru", "assert_all_requests_are_fired", "assert_call_count", "calls", "DELETE", "GET", "HEAD", "OPTIONS", "passthru_prefixes", "PATCH", "POST", "PUT", "registered", "remove", "replace", "reset", "response_callback", "start", "stop", "target", "upsert", ] activate = _default_mock.activate add = _default_mock.add add_callback = _default_mock.add_callback add_passthru = _default_mock.add_passthru assert_all_requests_are_fired = _default_mock.assert_all_requests_are_fired assert_call_count = _default_mock.assert_call_count calls = _default_mock.calls DELETE = _default_mock.DELETE GET = _default_mock.GET HEAD = _default_mock.HEAD OPTIONS = _default_mock.OPTIONS passthru_prefixes = _default_mock.passthru_prefixes PATCH = _default_mock.PATCH POST = _default_mock.POST PUT = _default_mock.PUT registered = _default_mock.registered remove = _default_mock.remove replace = _default_mock.replace reset = _default_mock.reset response_callback = _default_mock.response_callback start = _default_mock.start stop = _default_mock.stop target = _default_mock.target upsert = _default_mock.upsert ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1642535104.0 responses-0.18.0/responses/__init__.pyi0000644000175100001710000002436700000000000017500 0ustar00runnerdockerfrom collections import Sequence, Sized from typing import ( Any, Callable, Iterator, Mapping, Optional, NamedTuple, Protocol, TypeVar, Dict, List, Tuple, Union, Iterable, overload, Type ) from io import BufferedReader, BytesIO from re import Pattern from requests.adapters import HTTPResponse, PreparedRequest from requests.cookies import RequestsCookieJar from typing_extensions import Literal from unittest import mock as std_mock from urllib.parse import quote as quote from urllib3.response import HTTPHeaderDict # type: ignore # Not currently exposed in typestubs. from .matchers import urlencoded_params_matcher, json_params_matcher def _clean_unicode(url: str) -> str: ... def _cookies_from_headers(headers: Dict[str, str]) -> RequestsCookieJar: ... def _ensure_str(s: str) -> str: ... def _ensure_url_default_path( url: Union[Pattern[str], str] ) -> Union[Pattern[str], str]: ... def _get_url_and_path(url: str) -> str: ... def _handle_body( body: Optional[Union[bytes, BufferedReader, str]] ) -> Union[BufferedReader, BytesIO]: ... def _has_unicode(s: str) -> bool: ... def _is_string(s: Union[Pattern[str], str]) -> bool: ... def get_wrapped( func: Callable[..., Any], responses: RequestsMock, registry: Optional[Any] ) -> Callable[..., Any]: ... class Call(NamedTuple): request: PreparedRequest response: Any _Body = Union[str, BaseException, "Response", BufferedReader, bytes] MatcherIterable = Iterable[Callable[[Any], Callable[..., Any]]] class CallList(Sequence[Call], Sized): def __init__(self) -> None: self._calls = List[Call] ... def __iter__(self) -> Iterator[Call]: ... def __len__(self) -> int: ... def __getitem__(self, idx: int) -> Call: ... # type: ignore [override] def add(self, request: PreparedRequest, response: _Body) -> None: ... def reset(self) -> None: ... class FalseBool: def __bool__(self) -> bool: ... class BaseResponse: passthrough: bool = ... content_type: Optional[str] = ... headers: Optional[Mapping[str, str]] = ... stream: bool = ... method: Any = ... url: Any = ... match_querystring: Any = ... match: MatcherIterable = ... call_count: int = ... def __init__( self, method: str, url: Union[Pattern[str], str], match_querystring: Union[bool, object] = ..., match: MatcherIterable = ..., ) -> None: ... def __eq__(self, other: Any) -> bool: ... def __ne__(self, other: Any) -> bool: ... def _req_attr_matches( self, match: MatcherIterable, request: PreparedRequest ) -> Tuple[bool, str]: ... def _should_match_querystring( self, match_querystring_argument: Union[bool, object] ) -> bool: ... def _url_matches( self, url: Union[Pattern[str], str], other: str, match_querystring: bool = ... ) -> bool: ... def _url_matches_strict(self, url: str, other: str) -> bool: ... def get_headers(self) -> HTTPHeaderDict: ... # type: ignore def get_response(self, request: PreparedRequest) -> None: ... def matches(self, request: PreparedRequest) -> Tuple[bool, str]: ... class Response(BaseResponse): body: _Body = ... status: int = ... headers: Optional[Mapping[str, str]] = ... stream: bool = ... content_type: Optional[str] = ... auto_calculate_content_length: bool = ... def __init__( self, method: str, url: Union[Pattern[str], str], body: _Body = ..., json: Optional[Any] = ..., status: int = ..., headers: Optional[Mapping[str, str]] = ..., stream: bool = ..., content_type: Optional[str] = ..., auto_calculate_content_length: bool = ..., match_querystring: bool = ..., match: MatcherIterable = ..., ) -> None: ... def get_response( # type: ignore [override] self, request: PreparedRequest ) -> HTTPResponse: ... class CallbackResponse(BaseResponse): callback: Callable[[Any], Any] = ... stream: bool = ... content_type: Optional[str] = ... def __init__( self, method: str, url: Union[Pattern[str], str], callback: Callable[[Any], Any], stream: bool = ..., content_type: Optional[str] = ..., match_querystring: Union[bool, FalseBool] = ..., match: MatcherIterable = ..., ) -> None: ... def get_response( # type: ignore [override] self, request: PreparedRequest ) -> HTTPResponse: ... class PassthroughResponse(BaseResponse): passthrough: bool = ... class OriginalResponseShim: msg: Any = ... def __init__( # type: ignore [no-any-unimported] self, headers: HTTPHeaderDict ) -> None: ... def isclosed(self) -> bool: ... _F = TypeVar("_F", bound=Callable[..., Any]) class RequestsMock: DELETE: Literal["DELETE"] GET: Literal["GET"] HEAD: Literal["HEAD"] OPTIONS: Literal["OPTIONS"] PATCH: Literal["PATCH"] POST: Literal["POST"] PUT: Literal["PUT"] response_callback: Optional[Callable[[Any], Any]] = ... assert_all_requests_are_fired: Any = ... passthru_prefixes: Tuple[Union[str, Pattern[str]], ...] = ... target: Any = ... _matches: List[Any] def __init__( self, assert_all_requests_are_fired: bool = ..., response_callback: Optional[Callable[[Any], Any]] = ..., passthru_prefixes: Tuple[str, ...] = ..., target: str = ..., registry: Any = ..., ) -> None: self._patcher = Callable[[Any], Any] self._calls = CallList ... def reset(self) -> None: ... add: _Add add_passthru: _AddPassthru def remove( self, method_or_response: Optional[Union[str, Response]] = ..., url: Optional[Union[Pattern[str], str]] = ..., ) -> None: ... replace: _Replace upsert: _Upsert add_callback: _AddCallback @property def calls(self) -> CallList: ... def __enter__(self) -> RequestsMock: ... def __exit__(self, type: Any, value: Any, traceback: Any) -> bool: ... def activate(self, func: Optional[_F], registry: Optional[Any]) -> _F: ... def start(self) -> None: ... def stop(self, allow_assert: bool = ...) -> None: ... def assert_call_count(self, url: str, count: int) -> bool: ... def registered(self) -> List[Any]: ... def _set_registry(self, registry: Any) -> None: ... def _get_registry(self) -> Any: ... HeaderSet = Optional[Union[Mapping[str, str], List[Tuple[str, str]]]] class _Add(Protocol): def __call__( self, method: Optional[Union[str, BaseResponse]] = ..., url: Optional[Union[Pattern[str], str]] = ..., body: _Body = ..., json: Optional[Any] = ..., status: int = ..., headers: HeaderSet = ..., stream: bool = ..., content_type: Optional[str] = ..., auto_calculate_content_length: bool = ..., adding_headers: HeaderSet = ..., match_querystring: bool = ..., match: MatcherIterable = ..., ) -> None: ... class _AddCallback(Protocol): def __call__( self, method: str, url: Union[Pattern[str], str], callback: Callable[[PreparedRequest], Union[Exception, Tuple[int, Mapping[str, str], _Body]]], match_querystring: bool = ..., content_type: Optional[str] = ..., match: MatcherIterable = ..., ) -> None: ... class _AddPassthru(Protocol): def __call__( self, prefix: Union[Pattern[str], str] ) -> None: ... class _Remove(Protocol): def __call__( self, method_or_response: Optional[Union[str, BaseResponse]] = ..., url: Optional[Union[Pattern[str], str]] = ..., ) -> None: ... class _Replace(Protocol): def __call__( self, method_or_response: Optional[Union[str, BaseResponse]] = ..., url: Optional[Union[Pattern[str], str]] = ..., body: _Body = ..., json: Optional[Any] = ..., status: int = ..., headers: HeaderSet = ..., stream: bool = ..., content_type: Optional[str] = ..., adding_headers: HeaderSet = ..., match_querystring: bool = ..., match: MatcherIterable = ..., ) -> None: ... class _Upsert(Protocol): def __call__( self, method: Optional[Union[str, BaseResponse]] = ..., url: Optional[Union[Pattern[str], str]] = ..., body: _Body = ..., json: Optional[Any] = ..., status: int = ..., headers: HeaderSet = ..., stream: bool = ..., content_type: Optional[str] = ..., adding_headers: HeaderSet = ..., match_querystring: bool = ..., match: MatcherIterable = ..., ) -> None: ... class _Registered(Protocol): def __call__(self) -> List[Response]: ... class _Activate(Protocol): # see https://github.com/getsentry/responses/pull/469 for more details @overload def __call__(self, func: _F = ...) -> _F: ... # use this overload for scenario when 'responses.activate' is used @overload def __call__(self, registry: Type[Any] = ...) -> Callable[['_F'], '_F']: ... # use this overload for scenario when 'responses.activate(registry=)' is used activate: _Activate add: _Add add_callback: _AddCallback add_passthru: _AddPassthru assert_all_requests_are_fired: bool assert_call_count: Callable[[str, int], bool] calls: CallList DELETE: Literal["DELETE"] GET: Literal["GET"] HEAD: Literal["HEAD"] mock: RequestsMock _default_mock: RequestsMock OPTIONS: Literal["OPTIONS"] passthru_prefixes: Tuple[str, ...] PATCH: Literal["PATCH"] POST: Literal["POST"] PUT: Literal["PUT"] registered: _Registered remove: _Remove replace: _Replace reset: Callable[[], None] response_callback: Callable[[Any], Any] start: Callable[[], None] stop: Callable[..., None] target: Any upsert: _Upsert __all__ = [ "CallbackResponse", "Response", "RequestsMock", # Exposed by the RequestsMock class: "activate", "add", "add_callback", "add_passthru", "assert_all_requests_are_fired", "assert_call_count", "calls", "DELETE", "GET", "HEAD", "OPTIONS", "passthru_prefixes", "PATCH", "POST", "PUT", "registered", "remove", "replace", "reset", "response_callback", "start", "stop", "target", "upsert", ] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1642535104.0 responses-0.18.0/responses/matchers.py0000644000175100001710000002370100000000000017365 0ustar00runnerdockerimport json as json_module from requests import PreparedRequest from urllib.parse import parse_qsl, urlparse from requests.packages.urllib3.util.url import parse_url from json.decoder import JSONDecodeError def _create_key_val_str(input_dict): """ Returns string of format {'key': val, 'key2': val2} Function is called recursively for nested dictionaries :param input_dict: dictionary to transform :return: (str) reformatted string """ def list_to_str(input_list): """ Convert all list items to string. Function is called recursively for nested lists """ converted_list = [] for item in sorted(input_list, key=lambda x: str(x)): if isinstance(item, dict): item = _create_key_val_str(item) elif isinstance(item, list): item = list_to_str(item) converted_list.append(str(item)) list_str = ", ".join(converted_list) return "[" + list_str + "]" items_list = [] for key in sorted(input_dict.keys(), key=lambda x: str(x)): val = input_dict[key] if isinstance(val, dict): val = _create_key_val_str(val) elif isinstance(val, list): val = list_to_str(input_list=val) items_list.append("{}: {}".format(key, val)) key_val_str = "{{{}}}".format(", ".join(items_list)) return key_val_str def urlencoded_params_matcher(params): """ Matches URL encoded data :param params: (dict) data provided to 'data' arg of request :return: (func) matcher """ def match(request): reason = "" request_body = request.body qsl_body = dict(parse_qsl(request_body)) if request_body else {} params_dict = params or {} valid = params is None if request_body is None else params_dict == qsl_body if not valid: reason = "request.body doesn't match: {} doesn't match {}".format( _create_key_val_str(qsl_body), _create_key_val_str(params_dict) ) return valid, reason return match def json_params_matcher(params): """ Matches JSON encoded data :param params: (dict) JSON data provided to 'json' arg of request :return: (func) matcher """ def match(request): reason = "" request_body = request.body params_dict = params or {} try: if isinstance(request_body, bytes): request_body = request_body.decode("utf-8") json_body = json_module.loads(request_body) if request_body else {} valid = params is None if request_body is None else params_dict == json_body if not valid: reason = "request.body doesn't match: {} doesn't match {}".format( _create_key_val_str(json_body), _create_key_val_str(params_dict) ) except JSONDecodeError: valid = False reason = ( "request.body doesn't match: JSONDecodeError: Cannot parse request.body" ) return valid, reason return match def fragment_identifier_matcher(identifier): def match(request): reason = "" url_fragment = urlparse(request.url).fragment if identifier: url_fragment_qsl = sorted(parse_qsl(url_fragment)) identifier_qsl = sorted(parse_qsl(identifier)) valid = identifier_qsl == url_fragment_qsl else: valid = not url_fragment if not valid: reason = "URL fragment identifier is different: {} doesn't match {}".format( identifier, url_fragment ) return valid, reason return match def query_param_matcher(params): """ Matcher to match 'params' argument in request :param params: (dict), same as provided to request :return: (func) matcher """ def match(request): reason = "" request_params = request.params request_params_dict = request_params or {} params_dict = params or {} valid = ( params is None if request_params is None else params_dict == request_params_dict ) if not valid: reason = "Parameters do not match. {} doesn't match {}".format( _create_key_val_str(request_params_dict), _create_key_val_str(params_dict), ) return valid, reason return match def query_string_matcher(query): """ Matcher to match query string part of request :param query: (str), same as constructed by request :return: (func) matcher """ def match(request): reason = "" data = parse_url(request.url) request_query = data.query request_qsl = sorted(parse_qsl(request_query)) if request_query else {} matcher_qsl = sorted(parse_qsl(query)) if query else {} valid = not query if request_query is None else request_qsl == matcher_qsl if not valid: reason = "Query string doesn't match. {} doesn't match {}".format( _create_key_val_str(dict(request_qsl)), _create_key_val_str(dict(matcher_qsl)), ) return valid, reason return match def request_kwargs_matcher(kwargs): """ Matcher to match keyword arguments provided to request :param kwargs: (dict), keyword arguments, same as provided to request :return: (func) matcher """ def match(request): reason = "" kwargs_dict = kwargs or {} # validate only kwargs that were requested for comparison, skip defaults request_kwargs = { k: v for k, v in request.req_kwargs.items() if k in kwargs_dict } valid = ( not kwargs_dict if not request_kwargs else sorted(kwargs.items()) == sorted(request_kwargs.items()) ) if not valid: reason = "Arguments don't match: {} doesn't match {}".format( _create_key_val_str(request_kwargs), _create_key_val_str(kwargs_dict) ) return valid, reason return match def multipart_matcher(files, data=None): """ Matcher to match 'multipart/form-data' content-type. This function constructs request body and headers from provided 'data' and 'files' arguments and compares to actual request :param files: (dict), same as provided to request :param data: (dict), same as provided to request :return: (func) matcher """ if not files: raise TypeError("files argument cannot be empty") prepared = PreparedRequest() prepared.headers = {"Content-Type": ""} prepared.prepare_body(data=data, files=files) def get_boundary(content_type): """ Parse 'boundary' value from header. :param content_type: (str) headers["Content-Type"] value :return: (str) boundary value """ if "boundary=" not in content_type: return "" return content_type.split("boundary=")[1] def match(request): reason = "multipart/form-data doesn't match. " if "Content-Type" not in request.headers: return False, reason + "Request is missing the 'Content-Type' header" request_boundary = get_boundary(request.headers["Content-Type"]) prepared_boundary = get_boundary(prepared.headers["Content-Type"]) # replace boundary value in header and in body, since by default # urllib3.filepost.encode_multipart_formdata dynamically calculates # random boundary alphanumeric value request_content_type = request.headers["Content-Type"] prepared_content_type = prepared.headers["Content-Type"].replace( prepared_boundary, request_boundary ) request_body = request.body prepared_body = prepared.body if isinstance(prepared_body, bytes): # since headers always come as str, need to convert to bytes prepared_boundary = prepared_boundary.encode("utf-8") request_boundary = request_boundary.encode("utf-8") prepared_body = prepared_body.replace(prepared_boundary, request_boundary) headers_valid = prepared_content_type == request_content_type if not headers_valid: return ( False, reason + "Request headers['Content-Type'] is different. {} isn't equal to {}".format( request_content_type, prepared_content_type ), ) body_valid = prepared_body == request_body if not body_valid: return False, reason + "Request body differs. {} aren't equal {}".format( request_body, prepared_body ) return True, "" return match def header_matcher(headers, strict_match=False): """ Matcher to match 'headers' argument in request using the responses library. Because ``requests`` will send several standard headers in addition to what was specified by your code, request headers that are additional to the ones passed to the matcher are ignored by default. You can change this behaviour by passing ``strict_match=True``. :param headers: (dict), same as provided to request :param strict_match: (bool), whether headers in addition to those specified in the matcher should cause the match to fail. :return: (func) matcher """ def match(request): request_headers = request.headers or {} if not strict_match: # filter down to just the headers specified in the matcher request_headers = {k: v for k, v in request_headers.items() if k in headers} valid = sorted(headers.items()) == sorted(request_headers.items()) if not valid: return False, "Headers do not match: {} doesn't match {}".format( _create_key_val_str(request_headers), _create_key_val_str(headers) ) return valid, "" return match ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1642535104.0 responses-0.18.0/responses/matchers.pyi0000644000175100001710000000166200000000000017540 0ustar00runnerdockerfrom typing import ( Any, Callable, Optional, Dict, ) JSONDecodeError = ValueError def _create_key_val_str(input_dict: Dict[Any, Any]) -> str: ... def json_params_matcher( params: Optional[Dict[str, Any]] ) -> Callable[..., Any]: ... def urlencoded_params_matcher( params: Optional[Dict[str, str]] ) -> Callable[..., Any]: ... def query_param_matcher( params: Optional[Dict[str, str]] ) -> Callable[..., Any]: ... def query_string_matcher( query: Optional[str] ) -> Callable[..., Any]: ... def request_kwargs_matcher( kwargs: Optional[Dict[str, Any]] ) -> Callable[..., Any]: ... def multipart_matcher( files: Dict[str, Any], data: Optional[Dict[str, str]] = ... ) -> Callable[..., Any]: ... def header_matcher( headers: Dict[str, str], strict_match: bool = ... ) -> Callable[..., Any]: ... def fragment_identifier_matcher( identifier: Optional[str] ) -> Callable[..., Any]: ... ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1642535104.0 responses-0.18.0/responses/registries.py0000644000175100001710000000403100000000000017732 0ustar00runnerdockerfrom typing import ( TYPE_CHECKING, List, Optional, Tuple, ) if TYPE_CHECKING: # pragma: no cover # import only for linter run from requests import PreparedRequest from responses import BaseResponse class FirstMatchRegistry(object): def __init__(self) -> None: self._responses: List["BaseResponse"] = [] @property def registered(self) -> List["BaseResponse"]: return self._responses def reset(self) -> None: self._responses = [] def find( self, request: "PreparedRequest" ) -> Tuple[Optional["BaseResponse"], List[str]]: found = None found_match = None match_failed_reasons = [] for i, response in enumerate(self.registered): match_result, reason = response.matches(request) if match_result: if found is None: found = i found_match = response else: if self.registered[found].call_count > 0: # that assumes that some responses were added between calls self.registered.pop(found) found_match = response break # Multiple matches found. Remove & return the first response. return self.registered.pop(found), match_failed_reasons else: match_failed_reasons.append(reason) return found_match, match_failed_reasons def add(self, response: "BaseResponse") -> None: self.registered.append(response) def remove(self, response: "BaseResponse") -> None: while response in self.registered: self.registered.remove(response) def replace(self, response: "BaseResponse") -> None: try: index = self.registered.index(response) except ValueError: raise ValueError( "Response is not registered for URL {}".format(response.url) ) self.registered[index] = response ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1642535104.0 responses-0.18.0/responses/test_matchers.py0000644000175100001710000004645500000000000020437 0ustar00runnerdockerfrom __future__ import absolute_import, print_function, division, unicode_literals import pytest import requests import responses from requests.exceptions import ConnectionError from responses import matchers def assert_response(resp, body=None, content_type="text/plain"): assert resp.status_code == 200 assert resp.reason == "OK" assert resp.headers["Content-Type"] == content_type assert resp.text == body def assert_reset(): assert len(responses._default_mock.registered()) == 0 assert len(responses.calls) == 0 def test_query_string_matcher(): @responses.activate def run(): url = "http://example.com?test=1&foo=bar" responses.add( responses.GET, url, body=b"test", match=[matchers.query_string_matcher("test=1&foo=bar")], ) 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_request_matches_post_params(): @responses.activate def run(deprecated): if deprecated: json_params_matcher = getattr(responses, "json_params_matcher") urlencoded_params_matcher = getattr(responses, "urlencoded_params_matcher") else: json_params_matcher = matchers.json_params_matcher urlencoded_params_matcher = matchers.urlencoded_params_matcher responses.add( method=responses.POST, url="http://example.com/", body="one", match=[json_params_matcher({"page": {"name": "first", "type": "json"}})], ) responses.add( method=responses.POST, url="http://example.com/", body="two", match=[urlencoded_params_matcher({"page": "second", "type": "urlencoded"})], ) resp = requests.request( "POST", "http://example.com/", headers={"Content-Type": "x-www-form-urlencoded"}, data={"page": "second", "type": "urlencoded"}, ) assert_response(resp, "two") resp = requests.request( "POST", "http://example.com/", headers={"Content-Type": "application/json"}, json={"page": {"name": "first", "type": "json"}}, ) assert_response(resp, "one") with pytest.deprecated_call(): run(deprecated=True) assert_reset() run(deprecated=False) assert_reset() def test_request_matches_empty_body(): def run(): with responses.RequestsMock(assert_all_requests_are_fired=True) as rsps: # test that both json and urlencoded body are empty in matcher and in request rsps.add( method=responses.POST, url="http://example.com/", body="one", match=[matchers.json_params_matcher(None)], ) rsps.add( method=responses.POST, url="http://example.com/", body="two", match=[matchers.urlencoded_params_matcher(None)], ) resp = requests.request("POST", "http://example.com/") assert_response(resp, "one") resp = requests.request( "POST", "http://example.com/", headers={"Content-Type": "x-www-form-urlencoded"}, ) assert_response(resp, "two") with responses.RequestsMock(assert_all_requests_are_fired=False) as rsps: # test exception raise if matcher body is None but request data is not None rsps.add( method=responses.POST, url="http://example.com/", body="one", match=[matchers.json_params_matcher(None)], ) with pytest.raises(ConnectionError) as excinfo: resp = requests.request( "POST", "http://example.com/", json={"my": "data"}, headers={"Content-Type": "application/json"}, ) msg = str(excinfo.value) assert "request.body doesn't match: {my: data} doesn't match {}" in msg with responses.RequestsMock(assert_all_requests_are_fired=False) as rsps: rsps.add( method=responses.POST, url="http://example.com/", body="two", match=[matchers.urlencoded_params_matcher(None)], ) with pytest.raises(ConnectionError) as excinfo: resp = requests.request( "POST", "http://example.com/", headers={"Content-Type": "x-www-form-urlencoded"}, data={"page": "second", "type": "urlencoded"}, ) msg = str(excinfo.value) assert ( "request.body doesn't match: {page: second, type: urlencoded} doesn't match {}" in msg ) run() assert_reset() def test_request_matches_params(): @responses.activate def run(): url = "http://example.com/test" params = {"hello": "world", "I am": "a big test"} responses.add( method=responses.GET, url=url, body="test", match=[matchers.query_param_matcher(params)], match_querystring=False, ) # exchange parameter places for the test params = { "I am": "a big test", "hello": "world", } resp = requests.get(url, params=params) constructed_url = r"http://example.com/test?I+am=a+big+test&hello=world" assert resp.url == constructed_url assert resp.request.url == constructed_url resp_params = getattr(resp.request, "params") assert resp_params == params run() assert_reset() def test_fail_matchers_error(): """ Validate that Exception is raised if request does not match responses.matchers validate matchers.urlencoded_params_matcher validate matchers.json_params_matcher validate matchers.query_param_matcher validate matchers.request_kwargs_matcher :return: None """ def run(): with responses.RequestsMock(assert_all_requests_are_fired=False) as rsps: rsps.add( "POST", "http://example.com", match=[matchers.urlencoded_params_matcher({"foo": "bar"})], ) rsps.add( "POST", "http://example.com", match=[matchers.json_params_matcher({"fail": "json"})], ) with pytest.raises(ConnectionError) as excinfo: requests.post("http://example.com", data={"id": "bad"}) msg = str(excinfo.value) assert ( "request.body doesn't match: {id: bad} doesn't match {foo: bar}" in msg ) assert ( "request.body doesn't match: JSONDecodeError: Cannot parse request.body" in msg ) with responses.RequestsMock(assert_all_requests_are_fired=False) as rsps: rsps.add( "GET", "http://111.com", match=[matchers.query_param_matcher({"my": "params"})], ) rsps.add( method=responses.GET, url="http://111.com/", body="two", match=[matchers.json_params_matcher({"page": "one"})], ) with pytest.raises(ConnectionError) as excinfo: requests.get( "http://111.com", params={"id": "bad"}, json={"page": "two"} ) msg = str(excinfo.value) assert ( "Parameters do not match. {id: bad} doesn't match {my: params}" in msg ) assert ( "request.body doesn't match: {page: two} doesn't match {page: one}" in msg ) with responses.RequestsMock(assert_all_requests_are_fired=False) as rsps: req_kwargs = { "stream": True, "verify": False, } rsps.add( "GET", "http://111.com", match=[matchers.request_kwargs_matcher(req_kwargs)], ) with pytest.raises(ConnectionError) as excinfo: requests.get("http://111.com", stream=True) msg = str(excinfo.value) assert ( "Arguments don't match: " "{stream: True, verify: True} doesn't match {stream: True, verify: False}" ) in msg run() assert_reset() @pytest.mark.parametrize( "req_file,match_file", [ (b"Old World!", "Old World!"), ("Old World!", b"Old World!"), (b"Old World!", b"Old World!"), ("Old World!", "Old World!"), (b"\xacHello World!", b"\xacHello World!"), ], ) def test_multipart_matcher(req_file, match_file): @responses.activate def run(): req_data = {"some": "other", "data": "fields"} responses.add( responses.POST, url="http://httpbin.org/post", match=[ matchers.multipart_matcher( files={"file_name": match_file}, data=req_data ) ], ) resp = requests.post( "http://httpbin.org/post", data=req_data, files={"file_name": req_file} ) assert resp.status_code == 200 with pytest.raises(TypeError): responses.add( responses.POST, url="http://httpbin.org/post", match=[matchers.multipart_matcher(files={})], ) run() assert_reset() def test_multipart_matcher_fail(): """ Validate that Exception is raised if request does not match responses.matchers validate matchers.multipart_matcher :return: None """ def run(): # different file contents with responses.RequestsMock(assert_all_requests_are_fired=False) as rsps: req_data = {"some": "other", "data": "fields"} req_files = {"file_name": b"Old World!"} rsps.add( responses.POST, url="http://httpbin.org/post", match=[matchers.multipart_matcher(req_files, data=req_data)], ) with pytest.raises(ConnectionError) as excinfo: requests.post( "http://httpbin.org/post", data=req_data, files={"file_name": b"New World!"}, ) msg = str(excinfo.value) assert "multipart/form-data doesn't match. Request body differs." in msg assert ( r'\r\nContent-Disposition: form-data; name="file_name"; ' r'filename="file_name"\r\n\r\nOld World!\r\n' ) in msg assert ( r'\r\nContent-Disposition: form-data; name="file_name"; ' r'filename="file_name"\r\n\r\nNew World!\r\n' ) in msg # x-www-form-urlencoded request with responses.RequestsMock(assert_all_requests_are_fired=False) as rsps: req_data = {"some": "other", "data": "fields"} req_files = {"file_name": b"Old World!"} rsps.add( responses.POST, url="http://httpbin.org/post", match=[matchers.multipart_matcher(req_files, data=req_data)], ) with pytest.raises(ConnectionError) as excinfo: requests.post("http://httpbin.org/post", data=req_data) msg = str(excinfo.value) assert ( "multipart/form-data doesn't match. Request headers['Content-Type'] is different." in msg ) assert ( "application/x-www-form-urlencoded isn't equal to multipart/form-data; boundary=" in msg ) # empty body request with responses.RequestsMock(assert_all_requests_are_fired=False) as rsps: req_files = {"file_name": b"Old World!"} rsps.add( responses.POST, url="http://httpbin.org/post", match=[matchers.multipart_matcher(req_files)], ) with pytest.raises(ConnectionError) as excinfo: requests.post("http://httpbin.org/post") msg = str(excinfo.value) assert "Request is missing the 'Content-Type' header" in msg run() assert_reset() def test_query_string_matcher_raises(): """ Validate that Exception is raised if request does not match responses.matchers validate matchers.query_string_matcher :return: None """ def run(): with responses.RequestsMock(assert_all_requests_are_fired=False) as rsps: rsps.add( "GET", "http://111.com", match=[matchers.query_string_matcher("didi=pro")], ) with pytest.raises(ConnectionError) as excinfo: requests.get("http://111.com", params={"test": "1", "didi": "pro"}) msg = str(excinfo.value) assert ( "Query string doesn't match. {didi: pro, test: 1} doesn't match {didi: pro}" in msg ) run() assert_reset() def test_request_matches_headers(): @responses.activate def run(): url = "http://example.com/" responses.add( method=responses.GET, url=url, json={"success": True}, match=[matchers.header_matcher({"Accept": "application/json"})], ) responses.add( method=responses.GET, url=url, body="success", match=[matchers.header_matcher({"Accept": "text/plain"})], ) # the actual request can contain extra headers (requests always adds some itself anyway) resp = requests.get( url, headers={"Accept": "application/json", "Accept-Charset": "utf-8"} ) assert_response(resp, body='{"success": true}', content_type="application/json") resp = requests.get(url, headers={"Accept": "text/plain"}) assert_response(resp, body="success", content_type="text/plain") run() assert_reset() def test_request_matches_headers_no_match(): @responses.activate def run(): url = "http://example.com/" responses.add( method=responses.GET, url=url, json={"success": True}, match=[matchers.header_matcher({"Accept": "application/json"})], ) with pytest.raises(ConnectionError) as excinfo: requests.get(url, headers={"Accept": "application/xml"}) msg = str(excinfo.value) assert ( "Headers do not match: {Accept: application/xml} doesn't match " "{Accept: application/json}" ) in msg run() assert_reset() def test_request_matches_headers_strict_match(): @responses.activate def run(): url = "http://example.com/" responses.add( method=responses.GET, url=url, body="success", match=[ matchers.header_matcher({"Accept": "text/plain"}, strict_match=True) ], ) # requests will add some extra headers of its own, so we have to use prepared requests session = requests.Session() # make sure we send *just* the header we're expectin prepped = session.prepare_request( requests.Request( method="GET", url=url, ) ) prepped.headers.clear() prepped.headers["Accept"] = "text/plain" resp = session.send(prepped) assert_response(resp, body="success", content_type="text/plain") # include the "Accept-Charset" header, which will fail to match prepped = session.prepare_request( requests.Request( method="GET", url=url, ) ) prepped.headers.clear() prepped.headers["Accept"] = "text/plain" prepped.headers["Accept-Charset"] = "utf-8" with pytest.raises(ConnectionError) as excinfo: session.send(prepped) msg = str(excinfo.value) assert ( "Headers do not match: {Accept: text/plain, Accept-Charset: utf-8} " "doesn't match {Accept: text/plain}" ) in msg run() assert_reset() def test_fragment_identifier_matcher(): @responses.activate def run(): responses.add( responses.GET, "http://example.com", match=[matchers.fragment_identifier_matcher("test=1&foo=bar")], body=b"test", ) resp = requests.get("http://example.com#test=1&foo=bar") assert_response(resp, "test") run() assert_reset() def test_fragment_identifier_matcher_error(): @responses.activate def run(): responses.add( responses.GET, "http://example.com/", match=[matchers.fragment_identifier_matcher("test=1")], ) responses.add( responses.GET, "http://example.com/", match=[matchers.fragment_identifier_matcher(None)], ) with pytest.raises(ConnectionError) as excinfo: requests.get("http://example.com/#test=2") msg = str(excinfo.value) assert ( "URL fragment identifier is different: test=1 doesn't match test=2" ) in msg assert ( "URL fragment identifier is different: None doesn't match test=2" ) in msg run() assert_reset() def test_fragment_identifier_matcher_and_match_querystring(): @responses.activate def run(): url = "http://example.com?ab=xy&zed=qwe#test=1&foo=bar" responses.add( responses.GET, url, match_querystring=True, match=[matchers.fragment_identifier_matcher("test=1&foo=bar")], body=b"test", ) # two requests to check reversed order of fragment identifier resp = requests.get("http://example.com?ab=xy&zed=qwe#test=1&foo=bar") assert_response(resp, "test") resp = requests.get("http://example.com?zed=qwe&ab=xy#foo=bar&test=1") assert_response(resp, "test") run() assert_reset() def test_matchers_create_key_val_str(): """ Test that matchers._create_key_val_str does recursive conversion """ data = { "my_list": [ 1, 2, "a", {"key1": "val1", "key2": 2, 3: "test"}, "!", [["list", "nested"], {"nested": "dict"}], ], 1: 4, "test": "val", "high": {"nested": "nested_dict"}, } conv_str = matchers._create_key_val_str(data) reference = ( "{1: 4, high: {nested: nested_dict}, my_list: [!, 1, 2, [[list, nested], {nested: dict}], " "a, {3: test, key1: val1, key2: 2}], test: val}" ) assert conv_str == reference ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1642535104.0 responses-0.18.0/responses/test_registries.py0000644000175100001710000000355700000000000021005 0ustar00runnerdockerimport pytest import responses from responses import registries from responses.test_responses import assert_reset def test_set_registry_not_empty(): class CustomRegistry(registries.FirstMatchRegistry): pass @responses.activate def run(): url = "http://fizzbuzz/foo" responses.add(method=responses.GET, url=url) with pytest.raises(AttributeError) as excinfo: responses.mock._set_registry(CustomRegistry) msg = str(excinfo.value) assert "Cannot replace Registry, current registry has responses" in msg run() assert_reset() def test_set_registry(): class CustomRegistry(registries.FirstMatchRegistry): pass @responses.activate(registry=CustomRegistry) def run_with_registry(): assert type(responses.mock._get_registry()) == CustomRegistry @responses.activate def run(): # test that registry does not leak to another test assert type(responses.mock._get_registry()) == registries.FirstMatchRegistry run_with_registry() run() assert_reset() def test_set_registry_context_manager(): def run(): class CustomRegistry(registries.FirstMatchRegistry): pass with responses.RequestsMock( assert_all_requests_are_fired=False, registry=CustomRegistry ) as rsps: assert type(rsps._get_registry()) == CustomRegistry assert type(responses.mock._get_registry()) == registries.FirstMatchRegistry run() assert_reset() def test_registry_reset(): def run(): class CustomRegistry(registries.FirstMatchRegistry): pass with responses.RequestsMock( assert_all_requests_are_fired=False, registry=CustomRegistry ) as rsps: rsps._get_registry().reset() assert not rsps.registered() run() assert_reset() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1642535104.0 responses-0.18.0/responses/test_responses.py0000644000175100001710000016007300000000000020643 0ustar00runnerdocker# coding: utf-8 from __future__ import absolute_import, print_function, division, unicode_literals import inspect import os import re from io import BufferedReader, BytesIO import pytest import requests import responses from requests.exceptions import ConnectionError, HTTPError, ChunkedEncodingError from responses import ( BaseResponse, Response, PassthroughResponse, matchers, CallbackResponse, ) try: from mock import patch, Mock except ImportError: from unittest.mock import patch, Mock # type: ignore def assert_reset(): assert len(responses._default_mock.registered()) == 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 assert_params(resp, expected): assert hasattr(resp, "request"), "Missing request" assert hasattr( resp.request, "params" ), "Missing params on request that responses should add" assert getattr(resp.request, "params") == expected, "Incorrect parameters" 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" run() assert_reset() @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) as excinfo: responses.replace(responses.GET, replacement) assert "Response is not registered for URL %s" % replacement in str( excinfo.value ) run() assert_reset() def test_replace_response_object_error(): @responses.activate def run(): responses.add(Response(method=responses.GET, url="http://example.com/one")) with pytest.raises(ValueError) as excinfo: responses.replace( Response(method=responses.GET, url="http://example.com/two") ) assert "Response is not registered for URL http://example.com/two" in str( excinfo.value ) run() assert_reset() @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_upsert_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") if isinstance(replacement, BaseResponse): responses.upsert(replacement) else: responses.upsert(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/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_upsert_add(original, replacement): @responses.activate def run(): responses.add(responses.GET, "http://example.com/one", body="test1") if isinstance(replacement, BaseResponse): responses.upsert(replacement) else: responses.upsert(responses.GET, replacement, body="testtwo") resp = requests.get("http://example.com/two") assert_response(resp, "testtwo") 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_querystring_empty(): @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_match_querystring_missing_key(): @responses.activate def run(): responses.add(responses.GET, "http://example.com?foo=1&bar=2", body=b"test") with pytest.raises(ConnectionError): requests.get("http://example.com/?foo=1&baz=2") with pytest.raises(ConnectionError): requests.get("http://example.com/?bar=2&fez=1") 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=419) resp = requests.get(url) assert resp.status_code == 419 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", "Content-Type": "application/json", "Content-Length": "13", } 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 "bar" == resp.headers.get("foo") assert "application/json" == resp.headers.get("Content-Type") assert "13" == resp.headers.get("Content-Length") run() assert_reset() def test_callback_deprecated_stream_argument(): with pytest.deprecated_call(): CallbackResponse(responses.GET, "url", lambda x: x, stream=False) def test_callback_deprecated_match_querystring_argument(): with pytest.deprecated_call(): CallbackResponse(responses.GET, "url", lambda x: x, match_querystring=False) def test_callback_match_querystring_default_false(): """ Test to ensure that by default 'match_querystring' in 'add_callback' is set to False and does not raise deprecation see: https://github.com/getsentry/responses/issues/464 and related PR """ body = b"test callback" status = 200 params = {"hello": "world", "I am": "a big test"} 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, params=params) assert resp.text == "test callback" assert resp.status_code == status assert "foo" in resp.headers with pytest.warns(None) as record: run() # check that no deprecation warning was raised assert not record 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_callback_content_type_dict(): def request_callback(request): return ( 200, {"Content-Type": "application/json"}, b"foo", ) @responses.activate def run(): responses.add_callback("GET", "http://mockhost/.foo", callback=request_callback) resp = requests.get("http://mockhost/.foo") assert resp.text == "foo" assert resp.headers["content-type"] == "application/json" run() assert_reset() def test_callback_matchers(): def request_callback(request): return ( 200, {"Content-Type": "application/json"}, b"foo", ) @responses.activate def run(): req_data = {"some": "other", "data": "fields"} req_files = {"file_name": b"Old World!"} responses.add_callback( responses.POST, url="http://httpbin.org/post", match=[matchers.multipart_matcher(req_files, data=req_data)], callback=request_callback, ) resp = requests.post("http://httpbin.org/post", data=req_data, files=req_files) assert resp.text == "foo" assert resp.headers["content-type"] == "application/json" run() assert_reset() def test_callback_matchers_fail(): @responses.activate def run(): req_data = {"some": "other", "data": "fields"} req_files = {"file_name": b"Old World!"} responses.add_callback( responses.POST, url="http://httpbin.org/post", match=[matchers.multipart_matcher(req_files, data=req_data)], callback=lambda x: ( 0, {"a": ""}, "", ), ) with pytest.raises(ConnectionError) as exc: requests.post( "http://httpbin.org/post", data={"some": "other", "data": "wrong"}, files=req_files, ) assert "multipart/form-data doesn't match." in str(exc.value) run() assert_reset() def test_callback_content_type_tuple(): def request_callback(request): return ( 200, [("Content-Type", "application/json")], b"foo", ) @responses.activate def run(): responses.add_callback("GET", "http://mockhost/.foo", callback=request_callback) resp = requests.get("http://mockhost/.foo") assert resp.text == "foo" assert resp.headers["content-type"] == "application/json" 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_base_response_get_response(): resp = BaseResponse("GET", ".com") with pytest.raises(NotImplementedError): resp.get_response(requests.PreparedRequest()) 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) assert inspect.signature(test_function) == inspect.signature( decorated_test_function ) assert decorated_test_function(1, 2) == test_function(1, 2) assert decorated_test_function(3) == test_function(3) @pytest.fixture def my_fruit(): return "apple" @pytest.fixture def fruit_basket(my_fruit): return ["banana", my_fruit] @pytest.mark.usefixtures("my_fruit", "fruit_basket") class TestFixtures(object): """ Test that pytest fixtures work well with 'activate' decorator """ def test_function(self, my_fruit, fruit_basket): assert my_fruit in fruit_basket assert my_fruit == "apple" test_function_decorated = responses.activate(test_function) def test_activate_mock_interaction(): @patch("sys.stdout") def test_function(mock_stdout): return mock_stdout decorated_test_function = responses.activate(test_function) assert inspect.signature(test_function) == inspect.signature( decorated_test_function ) value = test_function() assert isinstance(value, Mock) value = decorated_test_function() assert isinstance(value, Mock) 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) assert inspect.signature(test_function) == inspect.signature( 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 set(resp.cookies.keys()) == set(["session_id"]) run() assert_reset() def test_response_cookies_secure(): body = b"test callback" status = 200 headers = {"set-cookie": "session_id=12345; a=b; c=d; secure"} 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 set(resp.cookies.keys()) == set(["session_id"]) run() assert_reset() def test_response_cookies_multiple(): body = b"test callback" status = 200 headers = [ ("set-cookie", "1P_JAR=2019-12-31-23; path=/; domain=.example.com; HttpOnly"), ("set-cookie", "NID=some=value; path=/; domain=.example.com; secure"), ] 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 set(resp.cookies.keys()) == set(["1P_JAR", "NID"]) assert resp.cookies["1P_JAR"] == "2019-12-31-23" assert resp.cookies["NID"] == "some=value" run() assert_reset() @pytest.mark.parametrize("request_stream", (True, False, None)) @pytest.mark.parametrize("responses_stream", (True, False, None)) def test_response_cookies_session(request_stream, responses_stream): @responses.activate def run(): url = "https://example.com/path" responses.add( responses.GET, url, headers=[ ("Set-cookie", "mycookie=cookieval; path=/; secure"), ], body="ok", stream=responses_stream, ) session = requests.session() resp = session.get(url, stream=request_stream) assert resp.text == "ok" assert resp.status_code == 200 assert "mycookie" in resp.cookies assert resp.cookies["mycookie"] == "cookieval" assert set(resp.cookies.keys()) == set(["mycookie"]) assert "mycookie" in session.cookies assert session.cookies["mycookie"] == "cookieval" assert set(session.cookies.keys()) == set(["mycookie"]) 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 getattr(resp, "_is_mocked") is True run() assert_reset() def test_response_filebody(): """ Adds the possibility to use actual (binary) files as responses """ def run(): current_file = os.path.abspath(__file__) with responses.RequestsMock() as m: with open(current_file, "r") as out: m.add(responses.GET, "http://example.com", body=out.read(), stream=True) resp = requests.get("http://example.com", stream=True) with open(current_file, "r") as out: assert resp.text == out.read() run() assert_reset() def test_use_stream_twice_to_double_raw_io(): @responses.activate def run(): url = "http://example.com" responses.add(responses.GET, url, body=b"42", stream=True) resp = requests.get(url, stream=True) assert resp.raw.read() == b"42" run() assert_reset() def test_assert_all_requests_are_fired(): def request_callback(request): raise BaseException() 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.value) # 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.registered()) == 1 requests.get("http://example.com") assert len(m.registered()) == 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.registered()) == 1 with pytest.raises(Exception): requests.get("http://example.com") assert len(m.registered()) == 1 with responses.RequestsMock(assert_all_requests_are_fired=True) as m: m.add_callback(responses.GET, "http://example.com", request_callback) assert len(m.registered()) == 1 with pytest.raises(BaseException): requests.get("http://example.com") assert len(m.registered()) == 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_handles_unicode_body(): url = "http://example.com/test" @responses.activate def run(): responses.add(responses.GET, url, body="михољско лето") resp = requests.get(url) assert_response(resp, "михољско лето", content_type="text/plain; charset=utf-8") run() assert_reset() def test_handles_buffered_reader_body(): url = "http://example.com/test" @responses.activate def run(): responses.add(responses.GET, url, body=BufferedReader(BytesIO(b"test"))) # type: ignore 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_content_length_error(monkeypatch): """ Currently 'requests' does not enforce content length validation, (validation that body length matches header). However, this could be expected in next major version, see https://github.com/psf/requests/pull/3563 Now user can manually patch URL3 lib to achieve the same See discussion in https://github.com/getsentry/responses/issues/394 """ @responses.activate def run(): responses.add( responses.GET, "http://example.com/api/123", json={"message": "this body is too large"}, adding_headers={"content-length": "2"}, ) with pytest.raises(ChunkedEncodingError) as exc: requests.get("http://example.com/api/123") assert "IncompleteRead" in str(exc.value) # Type errors here and on 1250 are ignored because the stubs for requests # are off https://github.com/python/typeshed/blob/f8501d33c737482a829c6db557a0be26895c5941 # /stubs/requests/requests/packages/__init__.pyi#L1 original_init = getattr(requests.packages.urllib3.HTTPResponse, "__init__") # type: ignore def patched_init(self, *args, **kwargs): kwargs["enforce_content_length"] = True original_init(self, *args, **kwargs) monkeypatch.setattr( requests.packages.urllib3.HTTPResponse, "__init__", patched_init # type: ignore ) run() assert_reset() def test_stream_with_none_chunk_size(): """ See discussion in https://github.com/getsentry/responses/issues/438 """ @responses.activate def run(): responses.add( responses.GET, "https://example.com", status=200, content_type="application/octet-stream", body=b"This is test", auto_calculate_content_length=True, ) res = requests.get("https://example.com", stream=True) for chunk in res.iter_content(chunk_size=None): assert chunk == b"This is test" 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_auto_calculate_content_length_string_body(): @responses.activate def run(): url = "http://example.com/" responses.add( responses.GET, url, body="test", auto_calculate_content_length=True ) resp = requests.get(url) assert_response(resp, "test") assert resp.headers["Content-Length"] == "4" run() assert_reset() def test_auto_calculate_content_length_bytes_body(): @responses.activate def run(): url = "http://example.com/" responses.add( responses.GET, url, body=b"test bytes", auto_calculate_content_length=True ) resp = requests.get(url) assert_response(resp, "test bytes") assert resp.headers["Content-Length"] == "10" run() assert_reset() def test_auto_calculate_content_length_json_body(): @responses.activate def run(): content_type = "application/json" url = "http://example.com/" responses.add( responses.GET, url, json={"message": "success"}, auto_calculate_content_length=True, ) resp = requests.get(url) assert_response(resp, '{"message": "success"}', content_type) assert resp.headers["Content-Length"] == "22" url = "http://example.com/1/" responses.add(responses.GET, url, json=[], auto_calculate_content_length=True) resp = requests.get(url) assert_response(resp, "[]", content_type) assert resp.headers["Content-Length"] == "2" run() assert_reset() def test_auto_calculate_content_length_unicode_body(): @responses.activate def run(): url = "http://example.com/test" responses.add( responses.GET, url, body="михољско лето", auto_calculate_content_length=True ) resp = requests.get(url) assert_response(resp, "михољско лето", content_type="text/plain; charset=utf-8") assert resp.headers["Content-Length"] == "25" run() assert_reset() def test_auto_calculate_content_length_doesnt_work_for_buffered_reader_body(): @responses.activate def run(): url = "http://example.com/test" responses.add( responses.GET, url, body=BufferedReader(BytesIO(b"testing")), # type: ignore auto_calculate_content_length=True, ) resp = requests.get(url) assert_response(resp, "testing") assert "Content-Length" not in resp.headers run() assert_reset() def test_auto_calculate_content_length_doesnt_override_existing_value(): @responses.activate def run(): url = "http://example.com/" responses.add( responses.GET, url, body="test", headers={"Content-Length": "2"}, auto_calculate_content_length=True, ) resp = requests.get(url) assert_response(resp, "test") assert resp.headers["Content-Length"] == "2" 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") responses.add(responses.GET, "http://example.com", body="fest") responses.add(responses.GET, "http://example.com", body="best") resp = requests.get("http://example.com") assert_response(resp, "test") resp = requests.get("http://example.com") assert_response(resp, "rest") resp = requests.get("http://example.com") assert_response(resp, "fest") resp = requests.get("http://example.com") assert_response(resp, "best") # After all responses are used, last response should be repeated resp = requests.get("http://example.com") assert_response(resp, "best") run() assert_reset() def test_multiple_responses_intermixed(): @responses.activate def run(): responses.add(responses.GET, "http://example.com", body="test") resp = requests.get("http://example.com") assert_response(resp, "test") responses.add(responses.GET, "http://example.com", body="rest") resp = requests.get("http://example.com") assert_response(resp, "rest") responses.add(responses.GET, "http://example.com", body="best") resp = requests.get("http://example.com") assert_response(resp, "best") # After all responses are used, last response should be repeated resp = requests.get("http://example.com") assert_response(resp, "best") 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_multiple_methods(): @responses.activate def run(): responses.add(responses.GET, "http://example.com/one", body="gotcha") responses.add(responses.POST, "http://example.com/one", body="posted") resp = requests.get("http://example.com/one") assert_response(resp, "gotcha") resp = requests.post("http://example.com/one") assert_response(resp, "posted") run() assert_reset() def test_passthrough_flag(httpserver): httpserver.serve_content("OK", headers={"Content-Type": "text/plain"}) response = Response(responses.GET, httpserver.url, body="MOCK") @responses.activate def run_passthrough(): responses.add(response) resp = requests.get(httpserver.url) assert_response(resp, "OK") @responses.activate def run_mocked(): responses.add(response) resp = requests.get(httpserver.url) assert_response(resp, "MOCK") run_mocked() assert_reset() response.passthrough = True run_passthrough() assert_reset() def test_passthrough_response(httpserver): httpserver.serve_content("OK", headers={"Content-Type": "text/plain"}) @responses.activate def run(): responses.add(PassthroughResponse(responses.GET, 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") assert len(responses.calls) == 3 responses.assert_call_count(httpserver.url, 1) run() assert_reset() def test_passthrough_response_stream(httpserver): httpserver.serve_content("OK", headers={"Content-Type": "text/plain"}) @responses.activate def run(): responses.add(PassthroughResponse(responses.GET, httpserver.url)) content_1 = requests.get(httpserver.url).content with requests.get(httpserver.url, stream=True) as resp: content_2 = resp.raw.read() assert content_1 == content_2 run() assert_reset() def test_passthru_prefixes(httpserver): httpserver.serve_content("OK", headers={"Content-Type": "text/plain"}) @responses.activate def run_constructor_argument(): with responses.RequestsMock(passthru_prefixes=(httpserver.url,)): resp = requests.get(httpserver.url) assert_response(resp, "OK") @responses.activate def run_property_setter(): with responses.RequestsMock() as m: m.passthru_prefixes = tuple([httpserver.url]) resp = requests.get(httpserver.url) assert_response(resp, "OK") run_constructor_argument() assert_reset() run_property_setter() 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_passthru_regex(httpserver): httpserver.serve_content("OK", headers={"Content-Type": "text/plain"}) @responses.activate def run(): responses.add_passthru(re.compile("{}/\\w+".format(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("{}/two".format(httpserver.url)) assert_response(resp, "OK") resp = requests.get("{}/three".format(httpserver.url)) assert_response(resp, "OK") run() assert_reset() def test_passthru_does_not_persist_across_tests(httpserver): """ passthru should be erased on exit from context manager see: https://github.com/getsentry/responses/issues/322 """ httpserver.serve_content("OK", headers={"Content-Type": "text/plain"}) @responses.activate def with_a_passthru(): assert not responses._default_mock.passthru_prefixes responses.add_passthru(re.compile(".*")) try: response = requests.get("https://example.com") except ConnectionError as err: # pragma: no cover if "Failed to establish" in str(err): # pragma: no cover pytest.skip("Cannot resolve DNS for example.com") # pragma: no cover raise err # pragma: no cover assert response.status_code == 200 @responses.activate def without_a_passthru(): assert not responses._default_mock.passthru_prefixes with pytest.raises(requests.exceptions.ConnectionError): requests.get("https://example.com") with_a_passthru() without_a_passthru() 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" def test_cookies_from_headers(): text = "こんにちは/世界" quoted_text = responses.quote(text) expected = {"x": "a", "y": quoted_text} headers = {"set-cookie": "; ".join(k + "=" + v for k, v in expected.items())} cookiejar = responses._cookies_from_headers(headers) for k, v in cookiejar.items(): assert isinstance(v, str) assert v == expected[k] @pytest.mark.parametrize( "url", ( "http://example.com", "http://example.com/some/path", "http://example.com/other/path/", ), ) def test_request_param(url): @responses.activate def run(): params = {"hello": "world", "example": "params"} responses.add( method=responses.GET, url="{0}?hello=world".format(url), body="test", match_querystring=False, ) resp = requests.get(url, params=params) assert_response(resp, "test") assert_params(resp, params) resp = requests.get(url) assert_response(resp, "test") assert_params(resp, {}) run() assert_reset() def test_request_param_with_multiple_values_for_the_same_key(): @responses.activate def run(): url = "http://example.com" params = {"key1": ["one", "two"], "key2": "three"} responses.add( method=responses.GET, url=url, body="test", ) resp = requests.get(url, params=params) assert_response(resp, "test") assert_params(resp, params) run() assert_reset() @pytest.mark.parametrize( "url", ("http://example.com", "http://example.com?hello=world") ) def test_assert_call_count(url): @responses.activate def run(): responses.add(responses.GET, url) responses.add(responses.GET, "http://example1.com") assert responses.assert_call_count(url, 0) is True with pytest.raises(AssertionError) as excinfo: responses.assert_call_count(url, 2) assert "Expected URL '{0}' to be called 2 times. Called 0 times.".format( url ) in str(excinfo.value) requests.get(url) assert responses.assert_call_count(url, 1) is True requests.get("http://example1.com") assert responses.assert_call_count(url, 1) is True requests.get(url) with pytest.raises(AssertionError) as excinfo: responses.assert_call_count(url, 3) assert "Expected URL '{0}' to be called 3 times. Called 2 times.".format( url ) in str(excinfo.value) run() assert_reset() def test_fail_request_error(): """ Validate that exception is raised if request URL/Method/kwargs don't match :return: """ def run(): with responses.RequestsMock(assert_all_requests_are_fired=False) as rsps: rsps.add("POST", "http://example1.com") rsps.add("GET", "http://example.com") with pytest.raises(ConnectionError) as excinfo: requests.post("http://example.com", data={"id": "bad"}) msg = str(excinfo.value) assert "- POST http://example1.com/ URL does not match" in msg assert "- GET http://example.com/ Method does not match" in msg run() assert_reset() @pytest.mark.parametrize( "response_params, expected_representation", [ ( {"method": responses.GET, "url": "http://example.com/"}, ( "" ), ), ( { "method": responses.POST, "url": "http://another-domain.com/", "content_type": "application/json", "status": 404, }, ( "" ), ), ( { "method": responses.PUT, "url": "http://abcd.com/", "content_type": "text/html", "status": 500, "headers": {"X-Test": "foo"}, "body": {"it_wont_be": "considered"}, }, ( "" ), ), ], ) def test_response_representations(response_params, expected_representation): response = Response(**response_params) assert str(response) == expected_representation assert repr(response) == expected_representation def test_mocked_responses_list_registered(): @responses.activate def run(): first_response = Response( responses.GET, "http://example.com/", body="", headers={"X-Test": "foo"}, status=404, ) second_response = Response( responses.GET, "http://example.com/", body="", headers={"X-Test": "foo"} ) third_response = Response( responses.POST, "http://anotherdomain.com/", ) responses.add(first_response) responses.add(second_response) responses.add(third_response) mocks_list = responses.registered() assert mocks_list == responses.mock.registered() assert mocks_list == [first_response, second_response, third_response] run() assert_reset() @pytest.mark.parametrize( "url,other_url", [ ("http://service-A/foo?q=fizz", "http://service-a/foo?q=fizz"), ("http://service-a/foo", "http://service-A/foo"), ("http://someHost-AwAy/", "http://somehost-away/"), ("http://fizzbuzz/foo", "http://fizzbuzz/foo"), ], ) def test_rfc_compliance(url, other_url): @responses.activate def run(): responses.add(method=responses.GET, url=url) resp = requests.request("GET", other_url) assert_response(resp, "") run() assert_reset() def test_requests_between_add(): @responses.activate def run(): responses.add(responses.GET, "https://example.com/", json={"response": "old"}) assert requests.get("https://example.com/").content == b'{"response": "old"}' assert requests.get("https://example.com/").content == b'{"response": "old"}' assert requests.get("https://example.com/").content == b'{"response": "old"}' responses.add(responses.GET, "https://example.com/", json={"response": "new"}) assert requests.get("https://example.com/").content == b'{"response": "new"}' assert requests.get("https://example.com/").content == b'{"response": "new"}' assert requests.get("https://example.com/").content == b'{"response": "new"}' run() assert_reset() ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1642535106.8147693 responses-0.18.0/responses.egg-info/0000755000175100001710000000000000000000000016674 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1642535106.0 responses-0.18.0/responses.egg-info/PKG-INFO0000644000175100001710000007062600000000000020004 0ustar00runnerdockerMetadata-Version: 2.1 Name: responses Version: 0.18.0 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 :: 3 Classifier: Programming Language :: Python :: 3.7 Classifier: Programming Language :: Python :: 3.8 Classifier: Programming Language :: Python :: 3.9 Classifier: Programming Language :: Python :: 3.10 Classifier: Topic :: Software Development Requires-Python: >=3.7 Description-Content-Type: text/x-rst Provides-Extra: tests License-File: LICENSE Responses ========= .. image:: https://img.shields.io/pypi/v/responses.svg :target: https://pypi.python.org/pypi/responses/ .. image:: https://img.shields.io/pypi/pyversions/responses.svg :target: https://pypi.org/project/responses/ .. image:: https://codecov.io/gh/getsentry/responses/branch/master/graph/badge.svg :target: https://codecov.io/gh/getsentry/responses/ A utility library for mocking out the ``requests`` Python library. .. note:: Responses requires Python 3.7 or newer, and requests >= 2.0 Table of Contents ----------------- .. contents:: 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``) DEPRECATED: Use ``responses.matchers.query_param_matcher`` or ``responses.matchers.query_string_matcher`` 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``) DEPRECATED: use ``stream`` argument in request directly auto_calculate_content_length (``bool``) Disabled by default. Automatically calculates the length of a supplied string or JSON body. match (``list``) A list of callbacks to match requests based on request attributes. Current module provides multiple matchers that you can use to match: * body contents in JSON format * body contents in URL encoded data format * request query parameters * request query string (similar to query parameters but takes string as input) * kwargs provided to request e.g. ``stream``, ``verify`` * 'multipart/form-data' content and headers in request * request headers * request fragment identifier Alternatively user can create custom matcher. Read more `Matching Requests`_ Matching Requests ----------------- Matching Request Body Contents ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ When adding responses for endpoints that are sent request data you can add matchers to ensure your code is sending the right parameters and provide different responses based on the request body contents. ``responses`` provides matchers for JSON and URL-encoded request bodies. URL-encoded data """""""""""""""" .. code-block:: python import responses import requests from responses import matchers @responses.activate def test_calc_api(): responses.add( responses.POST, url='http://calc.com/sum', body="4", match=[ matchers.urlencoded_params_matcher({"left": "1", "right": "3"}) ] ) requests.post("http://calc.com/sum", data={"left": 1, "right": 3}) JSON encoded data """"""""""""""""" Matching JSON encoded data can be done with ``matchers.json_params_matcher()``. .. code-block:: python import responses import requests from responses import matchers @responses.activate def test_calc_api(): responses.add( method=responses.POST, url="http://example.com/", body="one", match=[matchers.json_params_matcher({"page": {"name": "first", "type": "json"}})], ) resp = requests.request( "POST", "http://example.com/", headers={"Content-Type": "application/json"}, json={"page": {"name": "first", "type": "json"}}, ) Query Parameters Matcher ^^^^^^^^^^^^^^^^^^^^^^^^ Query Parameters as a Dictionary """""""""""""""""""""""""""""""" You can use the ``matchers.query_param_matcher`` function to match against the ``params`` request parameter. Just use the same dictionary as you will use in ``params`` argument in ``request``. Note, do not use query parameters as part of the URL. Avoid using ``match_querystring`` deprecated argument. .. code-block:: python import responses import requests from responses import matchers @responses.activate def test_calc_api(): url = "http://example.com/test" params = {"hello": "world", "I am": "a big test"} responses.add( method=responses.GET, url=url, body="test", match=[matchers.query_param_matcher(params)], match_querystring=False, ) resp = requests.get(url, params=params) constructed_url = r"http://example.com/test?I+am=a+big+test&hello=world" assert resp.url == constructed_url assert resp.request.url == constructed_url assert resp.request.params == params Query Parameters as a String """""""""""""""""""""""""""" As alternative, you can use query string value in ``matchers.query_string_matcher`` to match query parameters in your request .. code-block:: python import requests import responses from responses import matchers @responses.activate def my_func(): responses.add( responses.GET, "https://httpbin.org/get", match=[matchers.query_string_matcher("didi=pro&test=1")], ) resp = requests.get("https://httpbin.org/get", params={"test": 1, "didi": "pro"}) my_func() Request Keyword Arguments Matcher ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ To validate request arguments use the ``matchers.request_kwargs_matcher`` function to match against the request kwargs. Note, only arguments provided to ``matchers.request_kwargs_matcher`` will be validated. .. code-block:: python import responses import requests from responses import matchers with responses.RequestsMock(assert_all_requests_are_fired=False) as rsps: req_kwargs = { "stream": True, "verify": False, } rsps.add( "GET", "http://111.com", match=[matchers.request_kwargs_matcher(req_kwargs)], ) requests.get("http://111.com", stream=True) # >>> Arguments don't match: {stream: True, verify: True} doesn't match {stream: True, verify: False} Request multipart/form-data Data Validation ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ To validate request body and headers for ``multipart/form-data`` data you can use ``matchers.multipart_matcher``. The ``data``, and ``files`` parameters provided will be compared to the request: .. code-block:: python import requests import responses from responses.matchers import multipart_matcher @responses.activate def my_func(): req_data = {"some": "other", "data": "fields"} req_files = {"file_name": b"Old World!"} responses.add( responses.POST, url="http://httpbin.org/post", match=[multipart_matcher(req_files, data=req_data)] ) resp = requests.post("http://httpbin.org/post", files={"file_name": b"New World!"}) my_func() # >>> raises ConnectionError: multipart/form-data doesn't match. Request body differs. Request Fragment Identifier Validation ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ To validate request URL fragment identifier you can use ``matchers.fragment_identifier_matcher``. The matcher takes fragment string (everything after ``#`` sign) as input for comparison: .. code-block:: python import requests import responses from responses.matchers import fragment_identifier_matcher @responses.activate def run(): url = "http://example.com?ab=xy&zed=qwe#test=1&foo=bar" responses.add( responses.GET, url, match_querystring=True, match=[fragment_identifier_matcher("test=1&foo=bar")], body=b"test", ) # two requests to check reversed order of fragment identifier resp = requests.get("http://example.com?ab=xy&zed=qwe#test=1&foo=bar") resp = requests.get("http://example.com?zed=qwe&ab=xy#foo=bar&test=1") run() Request Headers Validation ^^^^^^^^^^^^^^^^^^^^^^^^^^ When adding responses you can specify matchers to ensure that your code is sending the right headers and provide different responses based on the request headers. .. code-block:: python import responses import requests from responses import matchers @responses.activate def test_content_type(): responses.add( responses.GET, url="http://example.com/", body="hello world", match=[ matchers.header_matcher({"Accept": "text/plain"}) ] ) responses.add( responses.GET, url="http://example.com/", json={"content": "hello world"}, match=[ matchers.header_matcher({"Accept": "application/json"}) ] ) # request in reverse order to how they were added! resp = requests.get("http://example.com/", headers={"Accept": "application/json"}) assert resp.json() == {"content": "hello world"} resp = requests.get("http://example.com/", headers={"Accept": "text/plain"}) assert resp.text == "hello world" Because ``requests`` will send several standard headers in addition to what was specified by your code, request headers that are additional to the ones passed to the matcher are ignored by default. You can change this behaviour by passing ``strict_match=True`` to the matcher to ensure that only the headers that you're expecting are sent and no others. Note that you will probably have to use a ``PreparedRequest`` in your code to ensure that ``requests`` doesn't include any additional headers. .. code-block:: python import responses import requests from responses import matchers @responses.activate def test_content_type(): responses.add( responses.GET, url="http://example.com/", body="hello world", match=[ matchers.header_matcher({"Accept": "text/plain"}, strict_match=True) ] ) # this will fail because requests adds its own headers with pytest.raises(ConnectionError): requests.get("http://example.com/", headers={"Accept": "text/plain"}) # a prepared request where you overwrite the headers before sending will work session = requests.Session() prepped = session.prepare_request( requests.Request( method="GET", url="http://example.com/", ) ) prepped.headers = {"Accept": "text/plain"} resp = session.send(prepped) assert resp.text == "hello world" Creating Custom Matcher ^^^^^^^^^^^^^^^^^^^^^^^ If your application requires other encodings or different data validation you can build your own matcher that returns ``Tuple[matches: bool, reason: str]``. Where boolean represents ``True`` or ``False`` if the request parameters match and the string is a reason in case of match failure. Your matcher can expect a ``PreparedRequest`` parameter to be provided by ``responses``. Note, ``PreparedRequest`` is customized and has additional attributes ``params`` and ``req_kwargs``. Response Registry --------------------------- By default, ``responses`` will search all registered ``Response`` objects and return a match. If only one ``Response`` is registered, the registry is kept unchanged. However, if multiple matches are found for the same request, then first match is returned and removed from registry. Such behavior is suitable for most of use cases, but to handle special conditions, you can implement custom registry which must follow interface of ``registries.FirstMatchRegistry``. Redefining the ``find`` method will allow you to create custom search logic and return appropriate ``Response`` Example that shows how to set custom registry .. code-block:: python import responses from responses import registries class CustomRegistry(registries.FirstMatchRegistry): pass """ Before tests: """ # using function decorator @responses.activate(registry=CustomRegistry) def run(): """ Within test: <__main__.CustomRegistry object> """ run() """ After test: """ # using context manager with responses.RequestsMock(registry=CustomRegistry) as rsps: """ In context manager: <__main__.CustomRegistry object> """ """ After exit from context manager: """ 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', ) You can see params passed in the original ``request`` in ``responses.calls[].request.params``: .. code-block:: python import responses import requests @responses.activate def test_request_params(): responses.add( method=responses.GET, url="http://example.com?hello=world", body="test", match_querystring=False, ) resp = requests.get('http://example.com', params={"hello": "world"}) assert responses.calls[0].request.params == {"hello": "world"} 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 Responses inside a unittest setUp() ----------------------------------- When run with unittest tests, this can be used to set up some generic class-level responses, that may be complemented by each test .. code-block:: python class TestMyApi(unittest.TestCase): def setUp(self): responses.add(responses.GET, 'https://example.com', body="within setup") # here go other self.responses.add(...) @responses.activate def test_my_func(self): responses.add( responses.GET, "https://httpbin.org/get", match=[matchers.query_param_matcher({"test": "1", "didi": "pro"})], body="within test" ) resp = requests.get("https://example.com") resp2 = requests.get("https://httpbin.org/get", params={"test": "1", "didi": "pro"}) print(resp.text) # >>> within setup print(resp2.text) # >>> within test 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') assert_call_count ----------------- Assert that the request was called exactly n times. .. code-block:: python import responses import requests @responses.activate def test_assert_call_count(): responses.add(responses.GET, "http://example.com") requests.get("http://example.com") assert responses.assert_call_count("http://example.com", 1) is True requests.get("http://example.com") with pytest.raises(AssertionError) as excinfo: responses.assert_call_count("http://example.com", 1) assert "Expected URL 'http://example.com' to be called 1 times. Called 2 times." in str(excinfo.value) 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 through real requests ----------------------------- In some cases you may wish to allow for certain requests to pass through responses and hit a real server. This can be done with the ``add_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. Pass through endpoints can be configured with regex patterns if you need to allow an entire domain or path subtree to send requests: .. code-block:: python responses.add_passthru(re.compile('https://percy.io/\\w+')) Lastly, you can use the `response.passthrough` attribute on `BaseResponse` or use ``PassthroughResponse`` to enable a response to behave as a pass through. .. code-block:: python # Enable passthrough for a single response response = Response(responses.GET, 'http://example.com', body='not used') response.passthrough = True responses.add(response) # Use PassthroughResponse response = PassthroughResponse(responses.GET, 'http://example.com') responses.add(response) Viewing/Modifying registered responses -------------------------------------- Registered responses are available as a public method of the RequestMock instance. It is sometimes useful for debugging purposes to view the stack of registered responses which can be accessed via ``responses.registered()``. 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} The ``upsert`` function allows a previously registered ``response`` to be changed like ``replace``. If the response is registered, the ``upsert`` function will registered it like ``add``. ``remove`` takes a ``method`` and ``url`` argument and will remove **all** matched responses from the registered list. Finally, ``reset`` will reset all registered responses. Contributing ------------ Environment Configuration ^^^^^^^^^^^^^^^^^^^^^^^^^ 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 Tests and Code Quality Validation ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ The easiest way to validate your code is to run tests via ``tox``. Current ``tox`` configuration runs the same checks that are used in GitHub Actions CI/CD pipeline. Please execute the following command line from the project root to validate your code against: * Unit tests in all Python versions that are supported by this project * Type validation via ``mypy`` * All ``pre-commit`` hooks .. code-block:: shell tox Alternatively, you can always run a single test. See documentation below. Unit tests """""""""" Responses uses `Pytest `_ for testing. You can run all tests by: .. code-block:: shell tox -e py37 tox -e py310 OR manually activate required version of Python and run .. code-block:: shell pytest And run a single test by: .. code-block:: shell pytest -k '' Type Validation """"""""""""""" To verify ``type`` compliance, run `mypy `_ linter: .. code-block:: shell tox -e mypy OR .. code-block:: shell mypy --config-file=./mypy.ini -p responses Code Quality and Style """""""""""""""""""""" To check code style and reformat it run: .. code-block:: shell tox -e precom OR .. code-block:: shell pre-commit run --all-files Note: on some OS, you have to use ``pre_commit`` ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1642535106.0 responses-0.18.0/responses.egg-info/SOURCES.txt0000644000175100001710000000071300000000000020561 0ustar00runnerdockerCHANGES LICENSE MANIFEST.in README.rst setup.cfg setup.py tox.ini responses/__init__.py responses/__init__.pyi responses/matchers.py responses/matchers.pyi responses/registries.py responses/test_matchers.py responses/test_registries.py responses/test_responses.py responses.egg-info/PKG-INFO responses.egg-info/SOURCES.txt responses.egg-info/dependency_links.txt responses.egg-info/not-zip-safe responses.egg-info/requires.txt responses.egg-info/top_level.txt././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1642535106.0 responses-0.18.0/responses.egg-info/dependency_links.txt0000644000175100001710000000000100000000000022742 0ustar00runnerdocker ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1642535106.0 responses-0.18.0/responses.egg-info/not-zip-safe0000644000175100001710000000000100000000000021122 0ustar00runnerdocker ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1642535106.0 responses-0.18.0/responses.egg-info/requires.txt0000644000175100001710000000021500000000000021272 0ustar00runnerdockerrequests<3.0,>=2.0 urllib3>=1.25.10 [tests] pytest>=4.6 coverage>=6.0.0 pytest-cov pytest-localserver flake8 types-mock types-requests mypy ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1642535106.0 responses-0.18.0/responses.egg-info/top_level.txt0000644000175100001710000000001200000000000021417 0ustar00runnerdockerresponses ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1642535106.8147693 responses-0.18.0/setup.cfg0000644000175100001710000000024500000000000015003 0ustar00runnerdocker[tool:pytest] addopts = --tb=short [bdist_wheel] universal = 0 [flake8] max-line-length = 100 [yapf] based_on_style = pep8 [egg_info] tag_build = tag_date = 0 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1642535104.0 responses-0.18.0/setup.py0000644000175100001710000000420400000000000014673 0ustar00runnerdocker#!/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 = [ "requests>=2.0,<3.0", "urllib3>=1.25.10", ] tests_require = [ "pytest>=4.6", "coverage >= 6.0.0", "pytest-cov", "pytest-localserver", "flake8", "types-mock", "types-requests", "mypy", ] 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.18.0", 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(), long_description_content_type="text/x-rst", packages=["responses"], zip_safe=False, python_requires=">=3.7", install_requires=install_requires, extras_require=extras_require, tests_require=tests_require, setup_requires=setup_requires, cmdclass={"test": PyTest}, package_data={"responses": ["py.typed", "__init__.pyi"]}, include_package_data=True, classifiers=[ "Intended Audience :: Developers", "Intended Audience :: System Administrators", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Topic :: Software Development", ], ) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1642535104.0 responses-0.18.0/tox.ini0000644000175100001710000000067200000000000014501 0ustar00runnerdocker[tox] envlist = py37,py38,py39,py310,mypy,precom [testenv] extras = tests commands = pytest . --cov responses --cov-report term-missing [testenv:mypy] description = Check types using 'mypy' basepython = python3.7 commands = python -m mypy --config-file=mypy.ini -p responses [testenv:precom] description = Run pre-commit hooks (black, flake, etc) basepython = python3.7 deps = pre-commit commands = pre-commit run --all-files