././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1736802182.8943353 responses-0.25.6/0000755000175100001660000000000014741277607013247 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1736802180.0 responses-0.25.6/CHANGES0000644000175100001660000003677514741277604014261 0ustar00runnerdocker0.25.5 ------ * Fix readme issue that prevented 0.25.4 from being published to pypi. 0.25.4 ------ * Responses can now match requests that use `data` with file-like objects. Files will be read as bytes and stored in the request mock. See #736 * `RequestsMock.matchers` was added. This property is an alias to `responses.matchers`. See #739 * Removed tests from packaged wheels. See #746 * Improved recorder API to ease use in REPL environments. See #745 0.25.3 ------ * Fixed `recorder` not saving and loading response headers with yaml files. See #715 0.25.2 ------ * Mulligan on 0.25.1 to run release pipeline correctly. * Added `matchers.body_matcher` for matching string request bodies. See #717 0.25.1 ------ * Fixed tests failures during RPM package builds. See #706 * Fix mocked HEAD responses that have `Content-Length` set. See #712 * Fixed error messages when matches fail: inputs are not sorted or reformatted. See #704 0.25.0 ------ * Added support for Python 3.12 * Fixed `matchers.header_matcher` not failing when a matched header is missing from the request. See #702 0.24.1 ------ * Reverted overloads removal * Added typing to `Call` attributes. * Fix socket issues (see #693) 0.24.0 ------ * Added `BaseResponse.calls` to access calls data of a separate mocked request. See #664 * Added `real_adapter_send` parameter to `RequestsMock` that will allow users to set through which function they would like to send real requests * Added support for re.Pattern based header matching. * Added support for gzipped response bodies to `json_params_matcher`. * Fix `Content-Type` headers issue when the header was duplicated. See #644 * Moved types-pyyaml dependency to `tests_requires` * Removed Python3.7 support 0.23.3 ------ * Allow urllib3>=1.25.10 0.23.2 ------ > This release is the last to support Python 3.7 * Updated dependency to urllib3>=2 and requests>=2.30.0. See #635 * Fixed issue when custom adapters were sending only positional args. See #642 * Expose `unbound_on_send` method in `RequestsMock` class. This method returns new function that is called by `RequestsMock` instead of original `send` method defined by any adapter. 0.23.1 ------ * Remove `tomli` import. See #630 0.23.0 ------ * Add Python 3.11 support * Fix type annotations of `CallList`. See #593 * `request` object is attached to any custom exception provided as `Response` `body` argument. See #588 * Fixed mocked responses leaking between tests when `assert_all_requests_are_fired` and a request was not fired. * [BETA] Default recorder format was changed to YAML. Added `responses.RequestsMock._parse_response_file` and `responses._recorder.Recorder.dump_to_file` methods that allow users to override default parser to eg toml, json 0.22.0 ------ * Update `requests` dependency to the version of 2.22.0 or higher. See #584. * [BETA] Added possibility to record responses to TOML files via `@_recorder.record(file_path="out.toml")` decorator. * [BETA] Added possibility to replay responses (populate registry) from TOML files via `responses._add_from_file(file_path="out.toml")` method. * Fix type for the `mock`'s patcher object. See #556 * Fix type annotation for `CallList` * Add `passthrough` argument to `BaseResponse` object. See #557 * Fix `registries` leak. See #563 * `OriginalResponseShim` is removed. See #585 * Add support for the `loose` version of `json_params_matcher` via named argument `strict_match`. See #551 * Add lists support as JSON objects in `json_params_matcher`. See #559 * Added project links to pypi listing. * `delete`, `get`, `head`, `options`, `patch`, `post`, `put` shortcuts are now implemented using `functools.partialmethod`. * Fix `MaxRetryError` exception. Replace exception by `RetryError` according to `requests` implementation. See #572. * Adjust error message when `Retry` is exhausted. See #580. 0.21.0 ------ * Add `threading.Lock()` to allow `responses` working with `threading` module. * Add `urllib3` `Retry` mechanism. See #135 * Removed internal `_cookies_from_headers` function * Now `add`, `upsert`, `replace` methods return registered response. `remove` method returns list of removed responses. * Added null value support in `urlencoded_params_matcher` via `allow_blank` keyword argument * Added strict version of decorator. Now you can apply `@responses.activate(assert_all_requests_are_fired=True)` to your function to validate that all requests were executed in the wrapped function. See #183 0.20.0 ------ * Deprecate `responses.assert_all_requests_are_fired`, `responses.passthru_prefixes`, `responses.target` since they are not actual properties of the class instance. Use `responses.mock.assert_all_requests_are_fired`, `responses.mock.passthru_prefixes`, `responses.mock.target` instead. * Fixed the issue when `reset()` method was called in not stopped mock. See #511 0.19.0 ------ * Added a registry that provides more strict ordering based on the invocation index. See `responses.registries.OrderedRegistry`. * Added shortcuts for each request method: delete, get, head, options, patch, post, put. For example, to add response for POST request you can use `responses.post()` instead of `responses.add(responses.POST)`. * Prevent `responses.activate` decorator to leak, if wrapped function called from within another wrapped function. Also, allow calling of above mentioned chain. See #481 for more details. * Expose `get_registry()` method of `RequestsMock` object. Replaces internal `_get_registry()`. * `query_param_matcher` can now accept dictionaries with `int` and `float` values. * Add support for the `loose` version of `query_param_matcher` via named argument `strict_match`. * Added support for `async/await` functions. * `response_callback` is no longer executed on exceptions raised by failed `Response`s * Change logic of `_get_url_and_path` to comply with RFC 3986. Now URL match occurs by matching schema, authority and path, where path is terminated by the first question mark ("?") or number sign ("#") character, or by the end of the URI. * An error is now raised when both `content_type` and `headers[content-type]` are provided as parameters. * When a request isn't matched the passthru prefixes are now included in error messages. 0.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=1736802180.0 responses-0.25.6/LICENSE0000644000175100001660000002512314741277604014254 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=1736802180.0 responses-0.25.6/MANIFEST.in0000644000175100001660000000021314741277604014776 0ustar00runnerdockerinclude README.rst CHANGES LICENSE recursive-include responses *.py include tox.ini recursive-include responses py.typed global-exclude *~ ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1736802182.8933353 responses-0.25.6/PKG-INFO0000644000175100001660000013267114741277607014356 0ustar00runnerdockerMetadata-Version: 2.1 Name: responses Version: 0.25.6 Summary: A utility library for mocking out the `requests` Python library. Home-page: https://github.com/getsentry/responses Author: David Cramer License: Apache 2.0 Project-URL: Bug Tracker, https://github.com/getsentry/responses/issues Project-URL: Changes, https://github.com/getsentry/responses/blob/master/CHANGES Project-URL: Documentation, https://github.com/getsentry/responses/blob/master/README.rst Project-URL: Source Code, https://github.com/getsentry/responses 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.8 Classifier: Programming Language :: Python :: 3.9 Classifier: Programming Language :: Python :: 3.10 Classifier: Programming Language :: Python :: 3.11 Classifier: Programming Language :: Python :: 3.12 Classifier: Topic :: Software Development Requires-Python: >=3.8 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://img.shields.io/pypi/dm/responses :target: https://pypi.python.org/pypi/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.8 or newer, and requests >= 2.30.0 Table of Contents ----------------- .. contents:: Installing ---------- ``pip install responses`` Deprecations and Migration Path ------------------------------- Here you will find a list of deprecated functionality and a migration path for each. Please ensure to update your code according to the guidance. .. list-table:: Deprecation and Migration :widths: 50 25 50 :header-rows: 1 * - Deprecated Functionality - Deprecated in Version - Migration Path * - ``responses.json_params_matcher`` - 0.14.0 - ``responses.matchers.json_params_matcher`` * - ``responses.urlencoded_params_matcher`` - 0.14.0 - ``responses.matchers.urlencoded_params_matcher`` * - ``stream`` argument in ``Response`` and ``CallbackResponse`` - 0.15.0 - Use ``stream`` argument in request directly. * - ``match_querystring`` argument in ``Response`` and ``CallbackResponse``. - 0.17.0 - Use ``responses.matchers.query_param_matcher`` or ``responses.matchers.query_string_matcher`` * - ``responses.assert_all_requests_are_fired``, ``responses.passthru_prefixes``, ``responses.target`` - 0.20.0 - Use ``responses.mock.assert_all_requests_are_fired``, ``responses.mock.passthru_prefixes``, ``responses.mock.target`` instead. Basics ------ The core of ``responses`` comes from registering mock responses and covering test function with ``responses.activate`` decorator. ``responses`` provides similar interface as ``requests``. Main Interface ^^^^^^^^^^^^^^ * responses.add(``Response`` or ``Response args``) - allows either to register ``Response`` object or directly provide arguments of ``Response`` object. See `Response Parameters`_ .. code-block:: python import responses import requests @responses.activate def test_simple(): # Register via 'Response' object rsp1 = responses.Response( method="PUT", url="http://example.com", ) responses.add(rsp1) # register via direct arguments 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") resp2 = requests.put("http://example.com") assert resp.json() == {"error": "not found"} assert resp.status_code == 404 assert resp2.status_code == 200 assert resp2.request.method == "PUT" 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") Shortcuts ^^^^^^^^^ Shortcuts provide a shorten version of ``responses.add()`` where method argument is prefilled * responses.delete(``Response args``) - register DELETE response * responses.get(``Response args``) - register GET response * responses.head(``Response args``) - register HEAD response * responses.options(``Response args``) - register OPTIONS response * responses.patch(``Response args``) - register PATCH response * responses.post(``Response args``) - register POST response * responses.put(``Response args``) - register PUT response .. code-block:: python import responses import requests @responses.activate def test_simple(): responses.get( "http://twitter.com/api/1/foobar", json={"type": "get"}, ) responses.post( "http://twitter.com/api/1/foobar", json={"type": "post"}, ) responses.patch( "http://twitter.com/api/1/foobar", json={"type": "patch"}, ) resp_get = requests.get("http://twitter.com/api/1/foobar") resp_post = requests.post("http://twitter.com/api/1/foobar") resp_patch = requests.patch("http://twitter.com/api/1/foobar") assert resp_get.json() == {"type": "get"} assert resp_post.json() == {"type": "post"} assert resp_patch.json() == {"type": "patch"} Responses as a context manager ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Instead of wrapping the whole function with decorator you can use 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 Response Parameters ------------------- 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`` or ``Exception``) The response body. Read more `Exception as 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 (``tuple``) An iterable (``tuple`` is recommended) 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`_ Exception as Response body -------------------------- 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.get("http://twitter.com/api/1/foobar", body=Exception("...")) with pytest.raises(Exception): requests.get("http://twitter.com/api/1/foobar") 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.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.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.get( url=url, body="test", match=[matchers.query_param_matcher(params)], ) 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 By default, matcher will validate that all parameters match strictly. To validate that only parameters specified in the matcher are present in original request use ``strict_match=False``. 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.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. Only following arguments are supported: ``timeout``, ``verify``, ``proxies``, ``stream``, ``cert``. 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.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.get( url, 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.get( url="http://example.com/", body="hello world", match=[matchers.header_matcher({"Accept": "text/plain"})], ) 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.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 --------------------------- Default 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. Ordered Registry ^^^^^^^^^^^^^^^^ In some scenarios it is important to preserve the order of the requests and responses. You can use ``registries.OrderedRegistry`` to force all ``Response`` objects to be dependent on the insertion order and invocation index. In following example we add multiple ``Response`` objects that target the same URL. However, you can see, that status code will depend on the invocation order. .. code-block:: python import requests import responses from responses.registries import OrderedRegistry @responses.activate(registry=OrderedRegistry) def test_invocation_index(): responses.get( "http://twitter.com/api/1/foobar", json={"msg": "not found"}, status=404, ) responses.get( "http://twitter.com/api/1/foobar", json={"msg": "OK"}, status=200, ) responses.get( "http://twitter.com/api/1/foobar", json={"msg": "OK"}, status=200, ) responses.get( "http://twitter.com/api/1/foobar", json={"msg": "not found"}, status=404, ) resp = requests.get("http://twitter.com/api/1/foobar") assert resp.status_code == 404 resp = requests.get("http://twitter.com/api/1/foobar") assert resp.status_code == 200 resp = requests.get("http://twitter.com/api/1/foobar") assert resp.status_code == 200 resp = requests.get("http://twitter.com/api/1/foobar") assert resp.status_code == 404 Custom Registry ^^^^^^^^^^^^^^^ Built-in ``registries`` are 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 print("Before tests:", responses.mock.get_registry()) """ Before tests: """ # using function decorator @responses.activate(registry=CustomRegistry) def run(): print("Within test:", responses.mock.get_registry()) """ Within test: <__main__.CustomRegistry object> """ run() print("After test:", responses.mock.get_registry()) """ After test: """ # using context manager with responses.RequestsMock(registry=CustomRegistry) as rsps: print("In context manager:", rsps.get_registry()) """ In context manager: <__main__.CustomRegistry object> """ print("After exit from context manager:", responses.mock.get_registry()) """ 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", ) Integration with unit test frameworks ------------------------------------- Responses as a ``pytest`` fixture ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Use the pytest-responses package to export ``responses`` as a pytest fixture. ``pip install pytest-responses`` You can then access it in a pytest script using: .. code-block:: python import pytest_responses def test_api(responses): 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 Add default responses for each test ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ When run with ``unittest`` tests, this can be used to set up some generic class-level responses, that may be complemented by each test. Similar interface could be applied in ``pytest`` framework. .. code-block:: python class TestMyApi(unittest.TestCase): def setUp(self): responses.get("https://example.com", body="within setup") # here go other self.responses.add(...) @responses.activate def test_my_func(self): 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 RequestMock methods: start, stop, reset ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ``responses`` has ``start``, ``stop``, ``reset`` methods very analogous to `unittest.mock.patch `_. These make it simpler to do requests mocking in ``setup`` methods or where you want to do multiple patches without nesting decorators or with statements. .. code-block:: python class TestUnitTestPatchSetup: def setup(self): """Creates ``RequestsMock`` instance and starts it.""" self.r_mock = responses.RequestsMock(assert_all_requests_are_fired=True) self.r_mock.start() # optionally some default responses could be registered self.r_mock.get("https://example.com", status=505) self.r_mock.put("https://example.com", status=506) def teardown(self): """Stops and resets RequestsMock instance. If ``assert_all_requests_are_fired`` is set to ``True``, will raise an error if some requests were not processed. """ self.r_mock.stop() self.r_mock.reset() def test_function(self): resp = requests.get("https://example.com") assert resp.status_code == 505 resp = requests.put("https://example.com") assert resp.status_code == 506 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 Request Call Count ------------------------- Assert based on ``Response`` object ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Each ``Response`` object has ``call_count`` attribute that could be inspected to check how many times each request was matched. .. code-block:: python @responses.activate def test_call_count_with_matcher(): rsp = responses.get( "http://www.example.com", match=(matchers.query_param_matcher({}),), ) rsp2 = responses.get( "http://www.example.com", match=(matchers.query_param_matcher({"hello": "world"}),), status=777, ) requests.get("http://www.example.com") resp1 = requests.get("http://www.example.com") requests.get("http://www.example.com?hello=world") resp2 = requests.get("http://www.example.com?hello=world") assert resp1.status_code == 200 assert resp2.status_code == 777 assert rsp.call_count == 2 assert rsp2.call_count == 2 Assert based on the exact URL ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Assert that the request was called exactly n times. .. code-block:: python import responses import requests @responses.activate def test_assert_call_count(): 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) ) @responses.activate def test_assert_call_count_always_match_qs(): responses.get("http://www.example.com") requests.get("http://www.example.com") requests.get("http://www.example.com?hello=world") # One call on each url, querystring is matched by default responses.assert_call_count("http://www.example.com", 1) is True responses.assert_call_count("http://www.example.com?hello=world", 1) is True Assert Request Calls data ------------------------- ``Request`` object has ``calls`` list which elements correspond to ``Call`` objects in the global list of ``Registry``. This can be useful when the order of requests is not guaranteed, but you need to check their correctness, for example in multithreaded applications. .. code-block:: python import concurrent.futures import responses import requests @responses.activate def test_assert_calls_on_resp(): rsp1 = responses.patch("http://www.foo.bar/1/", status=200) rsp2 = responses.patch("http://www.foo.bar/2/", status=400) rsp3 = responses.patch("http://www.foo.bar/3/", status=200) def update_user(uid, is_active): url = f"http://www.foo.bar/{uid}/" response = requests.patch(url, json={"is_active": is_active}) return response with concurrent.futures.ThreadPoolExecutor(max_workers=3) as executor: future_to_uid = { executor.submit(update_user, uid, is_active): uid for (uid, is_active) in [("3", True), ("2", True), ("1", False)] } for future in concurrent.futures.as_completed(future_to_uid): uid = future_to_uid[future] response = future.result() print(f"{uid} updated with {response.status_code} status code") assert len(responses.calls) == 3 # total calls count assert rsp1.call_count == 1 assert rsp1.calls[0] in responses.calls assert rsp1.calls[0].response.status_code == 200 assert json.loads(rsp1.calls[0].request.body) == {"is_active": False} assert rsp2.call_count == 1 assert rsp2.calls[0] in responses.calls assert rsp2.calls[0].response.status_code == 400 assert json.loads(rsp2.calls[0].request.body) == {"is_active": True} assert rsp3.call_count == 1 assert rsp3.calls[0] in responses.calls assert rsp3.calls[0].response.status_code == 200 assert json.loads(rsp3.calls[0].request.body) == {"is_active": True} 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.get("http://twitter.com/api/1/foobar", status=500) 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 URL Redirection --------------- In the following example you can see how to create a redirection chain and add custom exception that will be raised in the execution chain and contain the history of redirects. .. code-block:: A -> 301 redirect -> B B -> 301 redirect -> C C -> connection issue .. code-block:: python import pytest import requests import responses @responses.activate def test_redirect(): # create multiple Response objects where first two contain redirect headers rsp1 = responses.Response( responses.GET, "http://example.com/1", status=301, headers={"Location": "http://example.com/2"}, ) rsp2 = responses.Response( responses.GET, "http://example.com/2", status=301, headers={"Location": "http://example.com/3"}, ) rsp3 = responses.Response(responses.GET, "http://example.com/3", status=200) # register above generated Responses in ``response`` module responses.add(rsp1) responses.add(rsp2) responses.add(rsp3) # do the first request in order to generate genuine ``requests`` response # this object will contain genuine attributes of the response, like ``history`` rsp = requests.get("http://example.com/1") responses.calls.reset() # customize exception with ``response`` attribute my_error = requests.ConnectionError("custom error") my_error.response = rsp # update body of the 3rd response with Exception, this will be raised during execution rsp3.body = my_error with pytest.raises(requests.ConnectionError) as exc_info: requests.get("http://example.com/1") assert exc_info.value.args[0] == "custom error" assert rsp1.url in exc_info.value.response.history[0].url assert rsp2.url in exc_info.value.response.history[1].url Validate ``Retry`` mechanism ---------------------------- If you are using the ``Retry`` features of ``urllib3`` and want to cover scenarios that test your retry limits, you can test those scenarios with ``responses`` as well. The best approach will be to use an `Ordered Registry`_ .. code-block:: python import requests import responses from responses import registries from urllib3.util import Retry @responses.activate(registry=registries.OrderedRegistry) def test_max_retries(): url = "https://example.com" rsp1 = responses.get(url, body="Error", status=500) rsp2 = responses.get(url, body="Error", status=500) rsp3 = responses.get(url, body="Error", status=500) rsp4 = responses.get(url, body="OK", status=200) session = requests.Session() adapter = requests.adapters.HTTPAdapter( max_retries=Retry( total=4, backoff_factor=0.1, status_forcelist=[500], method_whitelist=["GET", "POST", "PATCH"], ) ) session.mount("https://", adapter) resp = session.get(url) assert resp.status_code == 200 assert rsp1.call_count == 1 assert rsp2.call_count == 1 assert rsp3.call_count == 1 assert rsp4.call_count == 1 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 ``passthrough`` argument of the ``Response`` object to force 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", 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.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. Coroutines and Multithreading ----------------------------- ``responses`` supports both Coroutines and Multithreading out of the box. Note, ``responses`` locks threading on ``RequestMock`` object allowing only single thread to access it. .. code-block:: python async def test_async_calls(): @responses.activate async def run(): 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 responses.calls[0].request.url == "http://twitter.com/api/1/foobar" await run() BETA Features ------------- Below you can find a list of BETA features. Although we will try to keep the API backwards compatible with released version, we reserve the right to change these APIs before they are considered stable. Please share your feedback via `GitHub Issues `_. Record Responses to files ^^^^^^^^^^^^^^^^^^^^^^^^^ You can perform real requests to the server and ``responses`` will automatically record the output to the file. Recorded data is stored in `YAML `_ format. Apply ``@responses._recorder.record(file_path="out.yaml")`` decorator to any function where you perform requests to record responses to ``out.yaml`` file. Following code .. code-block:: python import requests from responses import _recorder def another(): rsp = requests.get("https://httpstat.us/500") rsp = requests.get("https://httpstat.us/202") @_recorder.record(file_path="out.yaml") def test_recorder(): rsp = requests.get("https://httpstat.us/404") rsp = requests.get("https://httpbin.org/status/wrong") another() will produce next output: .. code-block:: yaml responses: - response: auto_calculate_content_length: false body: 404 Not Found content_type: text/plain method: GET status: 404 url: https://httpstat.us/404 - response: auto_calculate_content_length: false body: Invalid status code content_type: text/plain method: GET status: 400 url: https://httpbin.org/status/wrong - response: auto_calculate_content_length: false body: 500 Internal Server Error content_type: text/plain method: GET status: 500 url: https://httpstat.us/500 - response: auto_calculate_content_length: false body: 202 Accepted content_type: text/plain method: GET status: 202 url: https://httpstat.us/202 If you are in the REPL, you can also activete the recorder for all following responses: .. code-block:: python import requests from responses import _recorder _recorder.recorder.start() requests.get("https://httpstat.us/500") _recorder.recorder.dump_to_file("out.yaml") # you can stop or reset the recorder _recorder.recorder.stop() _recorder.recorder.reset() Replay responses (populate registry) from files ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ You can populate your active registry from a ``yaml`` file with recorded responses. (See `Record Responses to files`_ to understand how to obtain a file). To do that you need to execute ``responses._add_from_file(file_path="out.yaml")`` within an activated decorator or a context manager. The following code example registers a ``patch`` response, then all responses present in ``out.yaml`` file and a ``post`` response at the end. .. code-block:: python import responses @responses.activate def run(): responses.patch("http://httpbin.org") responses._add_from_file(file_path="out.yaml") responses.post("http://httpbin.org/form") run() 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 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1736802180.0 responses-0.25.6/README.rst0000644000175100001660000013043114741277604014735 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://img.shields.io/pypi/dm/responses :target: https://pypi.python.org/pypi/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.8 or newer, and requests >= 2.30.0 Table of Contents ----------------- .. contents:: Installing ---------- ``pip install responses`` Deprecations and Migration Path ------------------------------- Here you will find a list of deprecated functionality and a migration path for each. Please ensure to update your code according to the guidance. .. list-table:: Deprecation and Migration :widths: 50 25 50 :header-rows: 1 * - Deprecated Functionality - Deprecated in Version - Migration Path * - ``responses.json_params_matcher`` - 0.14.0 - ``responses.matchers.json_params_matcher`` * - ``responses.urlencoded_params_matcher`` - 0.14.0 - ``responses.matchers.urlencoded_params_matcher`` * - ``stream`` argument in ``Response`` and ``CallbackResponse`` - 0.15.0 - Use ``stream`` argument in request directly. * - ``match_querystring`` argument in ``Response`` and ``CallbackResponse``. - 0.17.0 - Use ``responses.matchers.query_param_matcher`` or ``responses.matchers.query_string_matcher`` * - ``responses.assert_all_requests_are_fired``, ``responses.passthru_prefixes``, ``responses.target`` - 0.20.0 - Use ``responses.mock.assert_all_requests_are_fired``, ``responses.mock.passthru_prefixes``, ``responses.mock.target`` instead. Basics ------ The core of ``responses`` comes from registering mock responses and covering test function with ``responses.activate`` decorator. ``responses`` provides similar interface as ``requests``. Main Interface ^^^^^^^^^^^^^^ * responses.add(``Response`` or ``Response args``) - allows either to register ``Response`` object or directly provide arguments of ``Response`` object. See `Response Parameters`_ .. code-block:: python import responses import requests @responses.activate def test_simple(): # Register via 'Response' object rsp1 = responses.Response( method="PUT", url="http://example.com", ) responses.add(rsp1) # register via direct arguments 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") resp2 = requests.put("http://example.com") assert resp.json() == {"error": "not found"} assert resp.status_code == 404 assert resp2.status_code == 200 assert resp2.request.method == "PUT" 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") Shortcuts ^^^^^^^^^ Shortcuts provide a shorten version of ``responses.add()`` where method argument is prefilled * responses.delete(``Response args``) - register DELETE response * responses.get(``Response args``) - register GET response * responses.head(``Response args``) - register HEAD response * responses.options(``Response args``) - register OPTIONS response * responses.patch(``Response args``) - register PATCH response * responses.post(``Response args``) - register POST response * responses.put(``Response args``) - register PUT response .. code-block:: python import responses import requests @responses.activate def test_simple(): responses.get( "http://twitter.com/api/1/foobar", json={"type": "get"}, ) responses.post( "http://twitter.com/api/1/foobar", json={"type": "post"}, ) responses.patch( "http://twitter.com/api/1/foobar", json={"type": "patch"}, ) resp_get = requests.get("http://twitter.com/api/1/foobar") resp_post = requests.post("http://twitter.com/api/1/foobar") resp_patch = requests.patch("http://twitter.com/api/1/foobar") assert resp_get.json() == {"type": "get"} assert resp_post.json() == {"type": "post"} assert resp_patch.json() == {"type": "patch"} Responses as a context manager ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Instead of wrapping the whole function with decorator you can use 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 Response Parameters ------------------- 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`` or ``Exception``) The response body. Read more `Exception as 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 (``tuple``) An iterable (``tuple`` is recommended) 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`_ Exception as Response body -------------------------- 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.get("http://twitter.com/api/1/foobar", body=Exception("...")) with pytest.raises(Exception): requests.get("http://twitter.com/api/1/foobar") 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.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.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.get( url=url, body="test", match=[matchers.query_param_matcher(params)], ) 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 By default, matcher will validate that all parameters match strictly. To validate that only parameters specified in the matcher are present in original request use ``strict_match=False``. 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.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. Only following arguments are supported: ``timeout``, ``verify``, ``proxies``, ``stream``, ``cert``. 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.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.get( url, 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.get( url="http://example.com/", body="hello world", match=[matchers.header_matcher({"Accept": "text/plain"})], ) 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.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 --------------------------- Default 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. Ordered Registry ^^^^^^^^^^^^^^^^ In some scenarios it is important to preserve the order of the requests and responses. You can use ``registries.OrderedRegistry`` to force all ``Response`` objects to be dependent on the insertion order and invocation index. In following example we add multiple ``Response`` objects that target the same URL. However, you can see, that status code will depend on the invocation order. .. code-block:: python import requests import responses from responses.registries import OrderedRegistry @responses.activate(registry=OrderedRegistry) def test_invocation_index(): responses.get( "http://twitter.com/api/1/foobar", json={"msg": "not found"}, status=404, ) responses.get( "http://twitter.com/api/1/foobar", json={"msg": "OK"}, status=200, ) responses.get( "http://twitter.com/api/1/foobar", json={"msg": "OK"}, status=200, ) responses.get( "http://twitter.com/api/1/foobar", json={"msg": "not found"}, status=404, ) resp = requests.get("http://twitter.com/api/1/foobar") assert resp.status_code == 404 resp = requests.get("http://twitter.com/api/1/foobar") assert resp.status_code == 200 resp = requests.get("http://twitter.com/api/1/foobar") assert resp.status_code == 200 resp = requests.get("http://twitter.com/api/1/foobar") assert resp.status_code == 404 Custom Registry ^^^^^^^^^^^^^^^ Built-in ``registries`` are 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 print("Before tests:", responses.mock.get_registry()) """ Before tests: """ # using function decorator @responses.activate(registry=CustomRegistry) def run(): print("Within test:", responses.mock.get_registry()) """ Within test: <__main__.CustomRegistry object> """ run() print("After test:", responses.mock.get_registry()) """ After test: """ # using context manager with responses.RequestsMock(registry=CustomRegistry) as rsps: print("In context manager:", rsps.get_registry()) """ In context manager: <__main__.CustomRegistry object> """ print("After exit from context manager:", responses.mock.get_registry()) """ 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", ) Integration with unit test frameworks ------------------------------------- Responses as a ``pytest`` fixture ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Use the pytest-responses package to export ``responses`` as a pytest fixture. ``pip install pytest-responses`` You can then access it in a pytest script using: .. code-block:: python import pytest_responses def test_api(responses): 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 Add default responses for each test ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ When run with ``unittest`` tests, this can be used to set up some generic class-level responses, that may be complemented by each test. Similar interface could be applied in ``pytest`` framework. .. code-block:: python class TestMyApi(unittest.TestCase): def setUp(self): responses.get("https://example.com", body="within setup") # here go other self.responses.add(...) @responses.activate def test_my_func(self): 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 RequestMock methods: start, stop, reset ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ``responses`` has ``start``, ``stop``, ``reset`` methods very analogous to `unittest.mock.patch `_. These make it simpler to do requests mocking in ``setup`` methods or where you want to do multiple patches without nesting decorators or with statements. .. code-block:: python class TestUnitTestPatchSetup: def setup(self): """Creates ``RequestsMock`` instance and starts it.""" self.r_mock = responses.RequestsMock(assert_all_requests_are_fired=True) self.r_mock.start() # optionally some default responses could be registered self.r_mock.get("https://example.com", status=505) self.r_mock.put("https://example.com", status=506) def teardown(self): """Stops and resets RequestsMock instance. If ``assert_all_requests_are_fired`` is set to ``True``, will raise an error if some requests were not processed. """ self.r_mock.stop() self.r_mock.reset() def test_function(self): resp = requests.get("https://example.com") assert resp.status_code == 505 resp = requests.put("https://example.com") assert resp.status_code == 506 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 Request Call Count ------------------------- Assert based on ``Response`` object ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Each ``Response`` object has ``call_count`` attribute that could be inspected to check how many times each request was matched. .. code-block:: python @responses.activate def test_call_count_with_matcher(): rsp = responses.get( "http://www.example.com", match=(matchers.query_param_matcher({}),), ) rsp2 = responses.get( "http://www.example.com", match=(matchers.query_param_matcher({"hello": "world"}),), status=777, ) requests.get("http://www.example.com") resp1 = requests.get("http://www.example.com") requests.get("http://www.example.com?hello=world") resp2 = requests.get("http://www.example.com?hello=world") assert resp1.status_code == 200 assert resp2.status_code == 777 assert rsp.call_count == 2 assert rsp2.call_count == 2 Assert based on the exact URL ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Assert that the request was called exactly n times. .. code-block:: python import responses import requests @responses.activate def test_assert_call_count(): 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) ) @responses.activate def test_assert_call_count_always_match_qs(): responses.get("http://www.example.com") requests.get("http://www.example.com") requests.get("http://www.example.com?hello=world") # One call on each url, querystring is matched by default responses.assert_call_count("http://www.example.com", 1) is True responses.assert_call_count("http://www.example.com?hello=world", 1) is True Assert Request Calls data ------------------------- ``Request`` object has ``calls`` list which elements correspond to ``Call`` objects in the global list of ``Registry``. This can be useful when the order of requests is not guaranteed, but you need to check their correctness, for example in multithreaded applications. .. code-block:: python import concurrent.futures import responses import requests @responses.activate def test_assert_calls_on_resp(): rsp1 = responses.patch("http://www.foo.bar/1/", status=200) rsp2 = responses.patch("http://www.foo.bar/2/", status=400) rsp3 = responses.patch("http://www.foo.bar/3/", status=200) def update_user(uid, is_active): url = f"http://www.foo.bar/{uid}/" response = requests.patch(url, json={"is_active": is_active}) return response with concurrent.futures.ThreadPoolExecutor(max_workers=3) as executor: future_to_uid = { executor.submit(update_user, uid, is_active): uid for (uid, is_active) in [("3", True), ("2", True), ("1", False)] } for future in concurrent.futures.as_completed(future_to_uid): uid = future_to_uid[future] response = future.result() print(f"{uid} updated with {response.status_code} status code") assert len(responses.calls) == 3 # total calls count assert rsp1.call_count == 1 assert rsp1.calls[0] in responses.calls assert rsp1.calls[0].response.status_code == 200 assert json.loads(rsp1.calls[0].request.body) == {"is_active": False} assert rsp2.call_count == 1 assert rsp2.calls[0] in responses.calls assert rsp2.calls[0].response.status_code == 400 assert json.loads(rsp2.calls[0].request.body) == {"is_active": True} assert rsp3.call_count == 1 assert rsp3.calls[0] in responses.calls assert rsp3.calls[0].response.status_code == 200 assert json.loads(rsp3.calls[0].request.body) == {"is_active": True} 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.get("http://twitter.com/api/1/foobar", status=500) 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 URL Redirection --------------- In the following example you can see how to create a redirection chain and add custom exception that will be raised in the execution chain and contain the history of redirects. .. code-block:: A -> 301 redirect -> B B -> 301 redirect -> C C -> connection issue .. code-block:: python import pytest import requests import responses @responses.activate def test_redirect(): # create multiple Response objects where first two contain redirect headers rsp1 = responses.Response( responses.GET, "http://example.com/1", status=301, headers={"Location": "http://example.com/2"}, ) rsp2 = responses.Response( responses.GET, "http://example.com/2", status=301, headers={"Location": "http://example.com/3"}, ) rsp3 = responses.Response(responses.GET, "http://example.com/3", status=200) # register above generated Responses in ``response`` module responses.add(rsp1) responses.add(rsp2) responses.add(rsp3) # do the first request in order to generate genuine ``requests`` response # this object will contain genuine attributes of the response, like ``history`` rsp = requests.get("http://example.com/1") responses.calls.reset() # customize exception with ``response`` attribute my_error = requests.ConnectionError("custom error") my_error.response = rsp # update body of the 3rd response with Exception, this will be raised during execution rsp3.body = my_error with pytest.raises(requests.ConnectionError) as exc_info: requests.get("http://example.com/1") assert exc_info.value.args[0] == "custom error" assert rsp1.url in exc_info.value.response.history[0].url assert rsp2.url in exc_info.value.response.history[1].url Validate ``Retry`` mechanism ---------------------------- If you are using the ``Retry`` features of ``urllib3`` and want to cover scenarios that test your retry limits, you can test those scenarios with ``responses`` as well. The best approach will be to use an `Ordered Registry`_ .. code-block:: python import requests import responses from responses import registries from urllib3.util import Retry @responses.activate(registry=registries.OrderedRegistry) def test_max_retries(): url = "https://example.com" rsp1 = responses.get(url, body="Error", status=500) rsp2 = responses.get(url, body="Error", status=500) rsp3 = responses.get(url, body="Error", status=500) rsp4 = responses.get(url, body="OK", status=200) session = requests.Session() adapter = requests.adapters.HTTPAdapter( max_retries=Retry( total=4, backoff_factor=0.1, status_forcelist=[500], method_whitelist=["GET", "POST", "PATCH"], ) ) session.mount("https://", adapter) resp = session.get(url) assert resp.status_code == 200 assert rsp1.call_count == 1 assert rsp2.call_count == 1 assert rsp3.call_count == 1 assert rsp4.call_count == 1 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 ``passthrough`` argument of the ``Response`` object to force 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", 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.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. Coroutines and Multithreading ----------------------------- ``responses`` supports both Coroutines and Multithreading out of the box. Note, ``responses`` locks threading on ``RequestMock`` object allowing only single thread to access it. .. code-block:: python async def test_async_calls(): @responses.activate async def run(): 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 responses.calls[0].request.url == "http://twitter.com/api/1/foobar" await run() BETA Features ------------- Below you can find a list of BETA features. Although we will try to keep the API backwards compatible with released version, we reserve the right to change these APIs before they are considered stable. Please share your feedback via `GitHub Issues `_. Record Responses to files ^^^^^^^^^^^^^^^^^^^^^^^^^ You can perform real requests to the server and ``responses`` will automatically record the output to the file. Recorded data is stored in `YAML `_ format. Apply ``@responses._recorder.record(file_path="out.yaml")`` decorator to any function where you perform requests to record responses to ``out.yaml`` file. Following code .. code-block:: python import requests from responses import _recorder def another(): rsp = requests.get("https://httpstat.us/500") rsp = requests.get("https://httpstat.us/202") @_recorder.record(file_path="out.yaml") def test_recorder(): rsp = requests.get("https://httpstat.us/404") rsp = requests.get("https://httpbin.org/status/wrong") another() will produce next output: .. code-block:: yaml responses: - response: auto_calculate_content_length: false body: 404 Not Found content_type: text/plain method: GET status: 404 url: https://httpstat.us/404 - response: auto_calculate_content_length: false body: Invalid status code content_type: text/plain method: GET status: 400 url: https://httpbin.org/status/wrong - response: auto_calculate_content_length: false body: 500 Internal Server Error content_type: text/plain method: GET status: 500 url: https://httpstat.us/500 - response: auto_calculate_content_length: false body: 202 Accepted content_type: text/plain method: GET status: 202 url: https://httpstat.us/202 If you are in the REPL, you can also activete the recorder for all following responses: .. code-block:: python import requests from responses import _recorder _recorder.recorder.start() requests.get("https://httpstat.us/500") _recorder.recorder.dump_to_file("out.yaml") # you can stop or reset the recorder _recorder.recorder.stop() _recorder.recorder.reset() Replay responses (populate registry) from files ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ You can populate your active registry from a ``yaml`` file with recorded responses. (See `Record Responses to files`_ to understand how to obtain a file). To do that you need to execute ``responses._add_from_file(file_path="out.yaml")`` within an activated decorator or a context manager. The following code example registers a ``patch`` response, then all responses present in ``out.yaml`` file and a ``post`` response at the end. .. code-block:: python import responses @responses.activate def run(): responses.patch("http://httpbin.org") responses._add_from_file(file_path="out.yaml") responses.post("http://httpbin.org/form") run() 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 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1736802180.0 responses-0.25.6/pyproject.toml0000644000175100001660000000034514741277604016162 0ustar00runnerdocker[build-system] # Suggest a reasonably modern floor for setuptools to ensure # the source dist package is assembled with all the expected resources. requires = ["setuptools >= 60", "wheel"] build-backend = "setuptools.build_meta" ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1736802182.8913352 responses-0.25.6/responses/0000755000175100001660000000000014741277607015270 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1736802180.0 responses-0.25.6/responses/__init__.py0000644000175100001660000012534714741277604017412 0ustar00runnerdockerimport inspect import json as json_module import logging from functools import partialmethod from functools import wraps from http import client from itertools import groupby from re import Pattern from threading import Lock as _ThreadingLock from typing import TYPE_CHECKING from typing import Any from typing import Callable from typing import Dict from typing import Iterable from typing import Iterator from typing import List from typing import Mapping from typing import NamedTuple from typing import Optional from typing import Sequence from typing import Sized from typing import Tuple from typing import Type from typing import Union from typing import overload from warnings import warn import yaml from requests.adapters import HTTPAdapter from requests.adapters import MaxRetryError from requests.exceptions import ConnectionError from requests.exceptions import RetryError from responses.matchers import json_params_matcher as _json_params_matcher from responses.matchers import query_string_matcher as _query_string_matcher from responses.matchers import urlencoded_params_matcher as _urlencoded_params_matcher from responses.registries import FirstMatchRegistry try: from typing_extensions import Literal except ImportError: # pragma: no cover from typing import Literal # type: ignore # pragma: no cover from io import BufferedReader from io import BytesIO from unittest import mock as std_mock from urllib.parse import parse_qsl from urllib.parse import quote from urllib.parse import urlsplit from urllib.parse import urlunparse from urllib.parse import urlunsplit from urllib3.response import HTTPHeaderDict from urllib3.response import HTTPResponse from urllib3.util.url import parse_url if TYPE_CHECKING: # pragma: no cover # import only for linter run import os from typing import Protocol from unittest.mock import _patch as _mock_patcher from requests import PreparedRequest from requests import models from urllib3 import Retry as _Retry class UnboundSend(Protocol): def __call__( self, adapter: HTTPAdapter, request: PreparedRequest, *args: Any, **kwargs: Any, ) -> models.Response: ... # Block of type annotations _Body = Union[str, BaseException, "Response", BufferedReader, bytes, None] _F = Callable[..., Any] _HeaderSet = Optional[Union[Mapping[str, str], List[Tuple[str, str]]]] _MatcherIterable = Iterable[Callable[..., Tuple[bool, str]]] _HTTPMethodOrResponse = Optional[Union[str, "BaseResponse"]] _URLPatternType = Union["Pattern[str]", str] _HTTPAdapterSend = Callable[ [ HTTPAdapter, PreparedRequest, bool, Union[float, Tuple[float, float], Tuple[float, None], None], Union[bool, str], Union[bytes, str, Tuple[Union[bytes, str], Union[bytes, str]], None], Optional[Mapping[str, str]], ], models.Response, ] class Call(NamedTuple): request: "PreparedRequest" response: "_Body" _real_send = HTTPAdapter.send _UNSET = object() logger = logging.getLogger("responses") class FalseBool: """Class to mock up built-in False boolean. Used for backwards compatibility, see https://github.com/getsentry/responses/issues/464 """ def __bool__(self) -> bool: return False def urlencoded_params_matcher(params: Optional[Dict[str, str]]) -> Callable[..., Any]: warn( "Function is deprecated. Use 'from responses.matchers import urlencoded_params_matcher'", DeprecationWarning, ) return _urlencoded_params_matcher(params) def json_params_matcher(params: Optional[Dict[str, Any]]) -> Callable[..., Any]: warn( "Function is deprecated. Use 'from responses.matchers import json_params_matcher'", DeprecationWarning, ) return _json_params_matcher(params) def _has_unicode(s: str) -> bool: return any(ord(char) > 128 for char in s) def _clean_unicode(url: str) -> str: """Clean up URLs, which use punycode to handle unicode chars. Applies percent encoding to URL path and query if required. Parameters ---------- url : str URL that should be cleaned from unicode Returns ------- str Cleaned URL """ 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 get_wrapped( func: Callable[..., Any], responses: "RequestsMock", *, registry: Optional[Any] = None, assert_all_requests_are_fired: Optional[bool] = None, ) -> Callable[..., Any]: """Wrap provided function inside ``responses`` context manager. Provides a synchronous or asynchronous wrapper for the function. Parameters ---------- func : Callable Function to wrap. responses : RequestsMock Mock object that is used as context manager. registry : FirstMatchRegistry, optional Custom registry that should be applied. See ``responses.registries`` assert_all_requests_are_fired : bool Raise an error if not all registered responses were executed. Returns ------- Callable Wrapped function """ assert_mock = std_mock.patch.object( target=responses, attribute="assert_all_requests_are_fired", new=assert_all_requests_are_fired, ) if inspect.iscoroutinefunction(func): # set asynchronous wrapper if requestor function is asynchronous @wraps(func) async def wrapper(*args: Any, **kwargs: Any) -> Any: # type: ignore[misc] if registry is not None: responses._set_registry(registry) with assert_mock, responses: return await func(*args, **kwargs) else: @wraps(func) def wrapper(*args: Any, **kwargs: Any) -> Any: # type: ignore[misc] if registry is not None: responses._set_registry(registry) with assert_mock, responses: # set 'assert_all_requests_are_fired' temporarily for a single run. # Mock automatically unsets to avoid leakage to another decorated # function since we still apply the value on 'responses.mock' object return func(*args, **kwargs) return wrapper class CallList(Sequence[Any], Sized): def __init__(self) -> None: self._calls: List[Call] = [] def __iter__(self) -> Iterator[Call]: return iter(self._calls) def __len__(self) -> int: return len(self._calls) @overload def __getitem__(self, idx: int) -> Call: ... @overload def __getitem__(self, idx: "slice[int, int, Optional[int]]") -> List[Call]: ... def __getitem__(self, idx: Union[int, slice]) -> Union[Call, List[Call]]: return self._calls[idx] def add(self, request: "PreparedRequest", response: "_Body") -> None: self._calls.append(Call(request, response)) def add_call(self, call: Call) -> None: self._calls.append(call) def reset(self) -> None: self._calls = [] def _ensure_url_default_path( url: "_URLPatternType", ) -> "_URLPatternType": """Add empty URL path '/' if doesn't exist. Examples -------- >>> _ensure_url_default_path("http://example.com") "http://example.com/" Parameters ---------- url : str or re.Pattern URL to validate. Returns ------- url : str or re.Pattern Modified URL if str or unchanged re.Pattern """ 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: str) -> str: """Construct URL only containing scheme, netloc and path by truncating other parts. This method complies with RFC 3986. Examples -------- >>> _get_url_and_path("http://example.com/path;segment?ab=xy&zed=qwe#test=1&foo=bar") "http://example.com/path;segment" Parameters ---------- url : str URL to parse. Returns ------- url : str URL with scheme, netloc and path """ url_parsed = urlsplit(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: Optional[Union[bytes, BufferedReader, str]] ) -> Union[BufferedReader, BytesIO]: """Generates `Response` body. Parameters ---------- body : str or bytes or BufferedReader Input data to generate `Response` body. Returns ------- body : BufferedReader or BytesIO `Response` body """ if isinstance(body, str): body = body.encode("utf-8") if isinstance(body, BufferedReader): return body data = BytesIO(body) # type: ignore[arg-type] def is_closed() -> bool: """ 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 # type: ignore[attr-defined] return data class BaseResponse: passthrough: bool = False content_type: Optional[str] = None headers: Optional[Mapping[str, str]] = None stream: Optional[bool] = False def __init__( self, method: str, url: "_URLPatternType", match_querystring: Union[bool, object] = None, match: "_MatcherIterable" = (), *, passthrough: bool = False, ) -> None: self.method: str = method # ensure the url has a default path set if the url is a string self.url: "_URLPatternType" = _ensure_url_default_path(url) if self._should_match_querystring(match_querystring): match = tuple(match) + ( _query_string_matcher(urlsplit(self.url).query), # type: ignore[arg-type] ) self.match: "_MatcherIterable" = match self._calls: CallList = CallList() self.passthrough = passthrough self.status: int = 200 self.body: "_Body" = "" def __eq__(self, other: Any) -> bool: 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: Any) -> bool: return not self.__eq__(other) def _should_match_querystring( self, match_querystring_argument: Union[bool, object] ) -> Union[bool, object]: 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(urlsplit(self.url).query) def _url_matches(self, url: "_URLPatternType", other: str) -> bool: """Compares two URLs. Compares only scheme, netloc and path. If 'url' is a re.Pattern, then checks that 'other' matches the pattern. Parameters ---------- url : Union["Pattern[str]", str] Reference URL or Pattern to compare. other : str URl that should be compared. Returns ------- bool True, if URLs are identical or 'other' matches the pattern. """ 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: "_MatcherIterable", request: "PreparedRequest" ) -> Tuple[bool, str]: for matcher in match: valid, reason = matcher(request) if not valid: return False, reason return True, "" def get_headers(self) -> HTTPHeaderDict: headers = HTTPHeaderDict() # Duplicate headers are legal # Add Content-Type if it exists and is not already in headers if self.content_type and ( not self.headers or "Content-Type" not in self.headers ): headers["Content-Type"] = self.content_type # Extend headers if they exist if self.headers: headers.extend(self.headers) return headers def get_response(self, request: "PreparedRequest") -> HTTPResponse: raise NotImplementedError def matches(self, request: "PreparedRequest") -> Tuple[bool, str]: if request.method != self.method: return False, "Method does not match" if not self._url_matches(self.url, str(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, "" @property def call_count(self) -> int: return len(self._calls) @property def calls(self) -> CallList: return self._calls def _form_response( body: Union[BufferedReader, BytesIO], headers: Optional[Mapping[str, str]], status: int, request_method: Optional[str], ) -> HTTPResponse: """ Function to generate `urllib3.response.HTTPResponse` object. The cookie handling functionality of the `requests` library relies on the response object having an original response object with the headers stored in the `msg` attribute. Instead of supplying a file-like object of type `HTTPMessage` for the headers, we provide the headers directly. This approach eliminates the need to parse the headers into a file-like object and then rely on the library to unparse it back. These additional conversions can introduce potential errors. """ data = BytesIO() data.close() """ The type `urllib3.response.HTTPResponse` is incorrect; we should use `http.client.HTTPResponse` instead. However, changing this requires opening a real socket to imitate the object. This may not be desired, as some users may want to completely restrict network access in their tests. See https://github.com/getsentry/responses/issues/691 """ orig_response = HTTPResponse( body=data, # required to avoid "ValueError: Unable to determine whether fp is closed." msg=headers, # type: ignore[arg-type] preload_content=False, ) return HTTPResponse( status=status, reason=client.responses.get(status, None), body=body, headers=headers, original_response=orig_response, # type: ignore[arg-type] # See comment above preload_content=False, request_method=request_method, ) class Response(BaseResponse): def __init__( self, method: str, url: "_URLPatternType", body: "_Body" = "", json: Optional[Any] = None, status: int = 200, headers: Optional[Mapping[str, str]] = None, stream: Optional[bool] = None, content_type: Union[str, object] = _UNSET, auto_calculate_content_length: bool = False, **kwargs: Any, ) -> None: super().__init__(method, url, **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" = body self.status: int = status self.headers: Optional[Mapping[str, str]] = headers if stream is not None: warn( "stream argument is deprecated. Use stream parameter in request directly", DeprecationWarning, ) self.stream: Optional[bool] = stream self.content_type: str = content_type # type: ignore[assignment] self.auto_calculate_content_length: bool = auto_calculate_content_length def get_response(self, request: "PreparedRequest") -> HTTPResponse: if self.body and isinstance(self.body, Exception): setattr(self.body, "request", request) raise self.body headers = self.get_headers() status = self.status assert not isinstance(self.body, (Response, BaseException)) body = _handle_body(self.body) if ( self.auto_calculate_content_length and isinstance(body, BytesIO) and "Content-Length" not in headers ): content_length = len(body.getvalue()) headers["Content-Length"] = str(content_length) return _form_response(body, headers, status, request.method) def __repr__(self) -> str: 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: str, url: "_URLPatternType", callback: Callable[[Any], Any], stream: Optional[bool] = None, content_type: Optional[str] = "text/plain", **kwargs: Any, ) -> None: super().__init__(method, url, **kwargs) self.callback = callback if stream is not None: warn( "stream argument is deprecated. Use stream parameter in request directly", DeprecationWarning, ) self.stream: Optional[bool] = stream self.content_type: Optional[str] = content_type def get_response(self, request: "PreparedRequest") -> HTTPResponse: 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 _form_response(body, headers, status, request.method) class PassthroughResponse(BaseResponse): def __init__(self, *args: Any, **kwargs: Any): super().__init__(*args, passthrough=True, **kwargs) class RequestsMock: DELETE: Literal["DELETE"] = "DELETE" GET: Literal["GET"] = "GET" HEAD: Literal["HEAD"] = "HEAD" OPTIONS: Literal["OPTIONS"] = "OPTIONS" PATCH: Literal["PATCH"] = "PATCH" POST: Literal["POST"] = "POST" PUT: Literal["PUT"] = "PUT" Response: Type[Response] = Response # Make the `matchers` name available under a RequestsMock instance from responses import matchers response_callback: Optional[Callable[[Any], Any]] = None def __init__( self, assert_all_requests_are_fired: bool = True, response_callback: Optional[Callable[[Any], Any]] = None, passthru_prefixes: Tuple[str, ...] = (), target: str = "requests.adapters.HTTPAdapter.send", registry: Type[FirstMatchRegistry] = FirstMatchRegistry, *, real_adapter_send: "_HTTPAdapterSend" = _real_send, ) -> None: self._calls: CallList = CallList() self.reset() self._registry: FirstMatchRegistry = registry() # call only after reset self.assert_all_requests_are_fired: bool = assert_all_requests_are_fired self.response_callback: Optional[Callable[[Any], Response]] = response_callback self.passthru_prefixes: Tuple[_URLPatternType, ...] = tuple(passthru_prefixes) self.target: str = target self._patcher: Optional["_mock_patcher[Any]"] = None self._thread_lock = _ThreadingLock() self._real_send = real_adapter_send def get_registry(self) -> FirstMatchRegistry: """Returns current registry instance with responses. Returns ------- FirstMatchRegistry Current registry instance with responses. """ return self._registry def _set_registry(self, new_registry: Type[FirstMatchRegistry]) -> None: """Replaces current registry with `new_registry`. Parameters ---------- new_registry : Type[FirstMatchRegistry] Class reference of the registry that should be set, eg OrderedRegistry """ 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) -> None: """Resets registry (including type), calls, passthru_prefixes to default values.""" self._registry = FirstMatchRegistry() self._calls.reset() self.passthru_prefixes = () def add( self, method: "_HTTPMethodOrResponse" = None, url: "Optional[_URLPatternType]" = None, body: "_Body" = "", adding_headers: "_HeaderSet" = None, *args: Any, **kwargs: Any, ) -> BaseResponse: """ >>> 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): return self._registry.add(method) if adding_headers is not None: kwargs.setdefault("headers", adding_headers) if ( "content_type" in kwargs and "headers" in kwargs and kwargs["headers"] is not None ): header_keys = [header.lower() for header in kwargs["headers"]] if "content-type" in header_keys: raise RuntimeError( "You cannot define both `content_type` and `headers[Content-Type]`." " Using the `content_type` kwarg is recommended." ) assert url is not None assert isinstance(method, str) response = Response(method=method, url=url, body=body, **kwargs) return self._registry.add(response) delete = partialmethod(add, DELETE) get = partialmethod(add, GET) head = partialmethod(add, HEAD) options = partialmethod(add, OPTIONS) patch = partialmethod(add, PATCH) post = partialmethod(add, POST) put = partialmethod(add, PUT) def _parse_response_file( self, file_path: "Union[str, bytes, os.PathLike[Any]]" ) -> "Dict[str, Any]": with open(file_path) as file: data = yaml.safe_load(file) return data def _add_from_file(self, file_path: "Union[str, bytes, os.PathLike[Any]]") -> None: data = self._parse_response_file(file_path) for rsp in data["responses"]: rsp = rsp["response"] self.add( method=rsp["method"], url=rsp["url"], body=rsp["body"], status=rsp["status"], headers=rsp["headers"] if "headers" in rsp else None, content_type=rsp["content_type"], auto_calculate_content_length=rsp["auto_calculate_content_length"], ) def add_passthru(self, prefix: "_URLPatternType") -> None: """ 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: >>> import re >>> 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: "_HTTPMethodOrResponse" = None, url: "Optional[_URLPatternType]" = None, ) -> List[BaseResponse]: """ 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: assert url is not None assert isinstance(method_or_response, str) response = BaseResponse(method=method_or_response, url=url) return self._registry.remove(response) def replace( self, method_or_response: "_HTTPMethodOrResponse" = None, url: "Optional[_URLPatternType]" = None, body: "_Body" = "", *args: Any, **kwargs: Any, ) -> BaseResponse: """ 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): response = method_or_response else: assert url is not None assert isinstance(method_or_response, str) response = Response(method=method_or_response, url=url, body=body, **kwargs) return self._registry.replace(response) def upsert( self, method_or_response: "_HTTPMethodOrResponse" = None, url: "Optional[_URLPatternType]" = None, body: "_Body" = "", *args: Any, **kwargs: Any, ) -> BaseResponse: """ 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: return self.replace(method_or_response, url, body, *args, **kwargs) except ValueError: return self.add(method_or_response, url, body, *args, **kwargs) def add_callback( self, method: str, url: "_URLPatternType", callback: Callable[ ["PreparedRequest"], Union[Exception, Tuple[int, Mapping[str, str], "_Body"]], ], match_querystring: Union[bool, FalseBool] = FalseBool(), content_type: Optional[str] = "text/plain", match: "_MatcherIterable" = (), ) -> None: self._registry.add( CallbackResponse( url=url, method=method, callback=callback, content_type=content_type, match_querystring=match_querystring, match=match, ) ) def registered(self) -> List["BaseResponse"]: return self._registry.registered @property def calls(self) -> CallList: return self._calls def __enter__(self) -> "RequestsMock": self.start() return self def __exit__(self, type: Any, value: Any, traceback: Any) -> bool: success = type is None try: self.stop(allow_assert=success) finally: self.reset() return success @overload def activate(self, func: "_F" = ...) -> "_F": """Overload for scenario when 'responses.activate' is used.""" @overload def activate( # type: ignore[misc] self, *, registry: Type[Any] = ..., assert_all_requests_are_fired: bool = ..., ) -> Callable[["_F"], "_F"]: """Overload for scenario when 'responses.activate(registry=, assert_all_requests_are_fired=True)' is used. See https://github.com/getsentry/responses/pull/469 for more details """ def activate( self, func: Optional["_F"] = None, *, registry: Optional[Type[Any]] = None, assert_all_requests_are_fired: bool = False, ) -> Union[Callable[["_F"], "_F"], "_F"]: if func is not None: return get_wrapped(func, self) def deco_activate(function: "_F") -> Callable[..., Any]: return get_wrapped( function, self, registry=registry, assert_all_requests_are_fired=assert_all_requests_are_fired, ) return deco_activate def _find_match( self, request: "PreparedRequest" ) -> Tuple[Optional["BaseResponse"], List[str]]: """ 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 """ with self._thread_lock: return self._registry.find(request) def _parse_request_params( self, url: str ) -> Dict[str, Union[str, int, float, List[Optional[Union[str, int, float]]]]]: params: Dict[str, Union[str, int, float, List[Any]]] = {} for key, val in groupby(parse_qsl(urlsplit(url).query), lambda kv: kv[0]): values = list(map(lambda x: x[1], val)) if len(values) == 1: values = values[0] # type: ignore[assignment] params[key] = values return params def _read_filelike_body( self, body: Union[str, bytes, BufferedReader, None] ) -> Union[str, bytes, None]: # Requests/urllib support multiple types of body, including file-like objects. # Read from the file if it's a file-like object to avoid storing a closed file # in the call list and allow the user to compare against the data that was in the # request. # See GH #719 if isinstance(body, str) or isinstance(body, bytes) or body is None: return body # Based on # https://github.com/urllib3/urllib3/blob/abbfbcb1dd274fc54b4f0a7785fd04d59b634195/src/urllib3/util/request.py#L220 if hasattr(body, "read") or isinstance(body, BufferedReader): return body.read() return body def _on_request( self, adapter: "HTTPAdapter", request: "PreparedRequest", *, retries: Optional["_Retry"] = None, **kwargs: Any, ) -> "models.Response": # 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) # type: ignore[attr-defined] request.req_kwargs = kwargs # type: ignore[attr-defined] request_url = str(request.url) request.body = self._read_filelike_body(request.body) 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 self._real_send(adapter, request, **kwargs) # type: ignore error_msg = ( "Connection refused by Responses - the call doesn't " "match any registered mock.\n\n" "Request: \n" f"- {request.method} {request_url}\n\n" "Available matches:\n" ) for i, m in enumerate(self.registered()): error_msg += "- {} {} {}\n".format( m.method, m.url, match_failed_reasons[i] ) if self.passthru_prefixes: error_msg += "Passthru prefixes:\n" for p in self.passthru_prefixes: error_msg += f"- {p}\n" response = ConnectionError(error_msg) response.request = request self._calls.add(request, response) raise response if match.passthrough: logger.info("request.passthrough-response", extra={"url": request_url}) response = self._real_send(adapter, request, **kwargs) # type: ignore else: try: response = adapter.build_response( # type: ignore[assignment] request, match.get_response(request) ) except BaseException as response: call = Call(request, response) self._calls.add_call(call) match.calls.add_call(call) raise if resp_callback: response = resp_callback(response) # type: ignore[misc] call = Call(request, response) # type: ignore[misc] self._calls.add_call(call) match.calls.add_call(call) retries = retries or adapter.max_retries # first validate that current request is eligible to be retried. # See ``urllib3.util.retry.Retry`` documentation. if retries.is_retry( method=response.request.method, status_code=response.status_code # type: ignore[misc] ): try: retries = retries.increment( method=response.request.method, # type: ignore[misc] url=response.url, # type: ignore[misc] response=response.raw, # type: ignore[misc] ) return self._on_request(adapter, request, retries=retries, **kwargs) except MaxRetryError as e: if retries.raise_on_status: """Since we call 'retries.increment()' by ourselves, we always set "error" argument equal to None, thus, MaxRetryError exception will be raised with ResponseError as a 'reason'. Here we're emulating the `if isinstance(e.reason, ResponseError):` branch found at: https://github.com/psf/requests/blob/ 177dd90f18a8f4dc79a7d2049f0a3f4fcc5932a0/requests/adapters.py#L549 """ raise RetryError(e, request=request) return response return response def unbound_on_send(self) -> "UnboundSend": def send( adapter: "HTTPAdapter", request: "PreparedRequest", *args: Any, **kwargs: Any, ) -> "models.Response": if args: # that probably means that the request was sent from the custom adapter # It is fully legit to send positional args from adapter, although, # `requests` implementation does it always with kwargs # See for more info: https://github.com/getsentry/responses/issues/642 try: kwargs["stream"] = args[0] kwargs["timeout"] = args[1] kwargs["verify"] = args[2] kwargs["cert"] = args[3] kwargs["proxies"] = args[4] except IndexError: # not all kwargs are required pass return self._on_request(adapter, request, **kwargs) return send def start(self) -> None: if self._patcher: # we must not override value of the _patcher if already applied # this prevents issues when one decorated function is called from # another decorated function return self._patcher = std_mock.patch(target=self.target, new=self.unbound_on_send()) self._patcher.start() def stop(self, allow_assert: bool = True) -> None: if self._patcher: # prevent stopping unstarted patchers self._patcher.stop() # once patcher is stopped, clean it. This is required to create a new # fresh patcher on self.start() self._patcher = None 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 {!r}".format( [(match.method, match.url) for match in not_called] ) ) def assert_call_count(self, url: str, count: int) -> bool: 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( f"Expected URL '{url}' to be called {count} times. Called {call_count} times." ) # 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_from_file", "add_callback", "add_passthru", "_deprecated_assert_all_requests_are_fired", "assert_call_count", "calls", "delete", "DELETE", "get", "GET", "head", "HEAD", "options", "OPTIONS", "_deprecated_passthru_prefixes", "patch", "PATCH", "post", "POST", "put", "PUT", "registered", "remove", "replace", "reset", "response_callback", "start", "stop", "_deprecated_target", "upsert", ] # expose only methods and/or read-only methods activate = _default_mock.activate add = _default_mock.add _add_from_file = _default_mock._add_from_file add_callback = _default_mock.add_callback add_passthru = _default_mock.add_passthru _deprecated_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 DELETE = _default_mock.DELETE get = _default_mock.get GET = _default_mock.GET head = _default_mock.head HEAD = _default_mock.HEAD options = _default_mock.options OPTIONS = _default_mock.OPTIONS _deprecated_passthru_prefixes = _default_mock.passthru_prefixes patch = _default_mock.patch PATCH = _default_mock.PATCH post = _default_mock.post POST = _default_mock.POST put = _default_mock.put 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 _deprecated_target = _default_mock.target upsert = _default_mock.upsert deprecated_names = ["assert_all_requests_are_fired", "passthru_prefixes", "target"] def __getattr__(name: str) -> Any: if name in deprecated_names: warn( f"{name} is deprecated. Please use 'responses.mock.{name}", DeprecationWarning, ) return globals()[f"_deprecated_{name}"] raise AttributeError(f"module {__name__} has no attribute {name}") ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1736802180.0 responses-0.25.6/responses/_recorder.py0000644000175100001660000001263014741277604017605 0ustar00runnerdockerfrom functools import wraps from typing import TYPE_CHECKING if TYPE_CHECKING: # pragma: no cover import os from typing import Any from typing import BinaryIO from typing import Callable from typing import Dict from typing import List from typing import Optional from typing import Type from typing import Union from responses import FirstMatchRegistry from responses import HTTPAdapter from responses import PreparedRequest from responses import models from responses import _F from responses import BaseResponse from io import TextIOWrapper import yaml from responses import RequestsMock from responses import Response from responses import _real_send from responses.registries import OrderedRegistry def _remove_nones(d: "Any") -> "Any": if isinstance(d, dict): return {k: _remove_nones(v) for k, v in d.items() if v is not None} if isinstance(d, list): return [_remove_nones(i) for i in d] return d def _remove_default_headers(data: "Any") -> "Any": """ It would be too verbose to store these headers in the file generated by the record functionality. """ if isinstance(data, dict): keys_to_remove = [ "Content-Length", "Content-Type", "Date", "Server", "Connection", "Content-Encoding", ] for i, response in enumerate(data["responses"]): for key in keys_to_remove: if key in response["response"]["headers"]: del data["responses"][i]["response"]["headers"][key] if not response["response"]["headers"]: del data["responses"][i]["response"]["headers"] return data def _dump( registered: "List[BaseResponse]", destination: "Union[BinaryIO, TextIOWrapper]", dumper: "Callable[[Union[Dict[Any, Any], List[Any]], Union[BinaryIO, TextIOWrapper]], Any]", ) -> None: data: Dict[str, Any] = {"responses": []} for rsp in registered: try: content_length = rsp.auto_calculate_content_length # type: ignore[attr-defined] data["responses"].append( { "response": { "method": rsp.method, "url": rsp.url, "body": rsp.body, "status": rsp.status, "headers": rsp.headers, "content_type": rsp.content_type, "auto_calculate_content_length": content_length, } } ) except AttributeError as exc: # pragma: no cover raise AttributeError( "Cannot dump response object." "Probably you use custom Response object that is missing required attributes" ) from exc dumper(_remove_default_headers(_remove_nones(data)), destination) class Recorder(RequestsMock): def __init__( self, *, target: str = "requests.adapters.HTTPAdapter.send", registry: "Type[FirstMatchRegistry]" = OrderedRegistry, ) -> None: super().__init__(target=target, registry=registry) def reset(self) -> None: self._registry = OrderedRegistry() def record( self, *, file_path: "Union[str, bytes, os.PathLike[Any]]" = "response.yaml" ) -> "Union[Callable[[_F], _F], _F]": def deco_record(function: "_F") -> "Callable[..., Any]": @wraps(function) def wrapper(*args: "Any", **kwargs: "Any") -> "Any": # type: ignore[misc] with self: ret = function(*args, **kwargs) self.dump_to_file( file_path=file_path, registered=self.get_registry().registered ) return ret return wrapper return deco_record def dump_to_file( self, file_path: "Union[str, bytes, os.PathLike[Any]]", *, registered: "Optional[List[BaseResponse]]" = None, ) -> None: """Dump the recorded responses to a file.""" if registered is None: registered = self.get_registry().registered with open(file_path, "w") as file: _dump(registered, file, yaml.dump) def _on_request( self, adapter: "HTTPAdapter", request: "PreparedRequest", **kwargs: "Any", ) -> "models.Response": # 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) # type: ignore[attr-defined] request.req_kwargs = kwargs # type: ignore[attr-defined] requests_response = _real_send(adapter, request, **kwargs) headers_values = { key: value for key, value in requests_response.headers.items() } responses_response = Response( method=str(request.method), url=str(requests_response.request.url), status=requests_response.status_code, body=requests_response.text, headers=headers_values, ) self._registry.add(responses_response) return requests_response def stop(self, allow_assert: bool = True) -> None: super().stop(allow_assert=False) recorder = Recorder() record = recorder.record ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1736802180.0 responses-0.25.6/responses/matchers.py0000644000175100001660000003252614741277604017455 0ustar00runnerdockerimport gzip import json as json_module import re from json.decoder import JSONDecodeError from typing import Any from typing import Callable from typing import List from typing import Mapping from typing import MutableMapping from typing import Optional from typing import Pattern from typing import Tuple from typing import Union from urllib.parse import parse_qsl from urllib.parse import urlparse from requests import PreparedRequest from urllib3.util.url import parse_url def _filter_dict_recursively( dict1: Mapping[Any, Any], dict2: Mapping[Any, Any] ) -> Mapping[Any, Any]: filtered_dict = {} for k, val in dict1.items(): if k in dict2: if isinstance(val, dict): val = _filter_dict_recursively(val, dict2[k]) filtered_dict[k] = val return filtered_dict def body_matcher(params: str, *, allow_blank: bool = False) -> Callable[..., Any]: def match(request: PreparedRequest) -> Tuple[bool, str]: reason = "" if isinstance(request.body, bytes): request_body = request.body.decode("utf-8") else: request_body = str(request.body) valid = True if request_body == params else False if not valid: reason = f"request.body doesn't match {params} doesn't match {request_body}" return valid, reason return match def urlencoded_params_matcher( params: Optional[Mapping[str, str]], *, allow_blank: bool = False ) -> Callable[..., Any]: """ Matches URL encoded data :param params: (dict) data provided to 'data' arg of request :return: (func) matcher """ def match(request: PreparedRequest) -> Tuple[bool, str]: reason = "" request_body = request.body qsl_body = ( dict(parse_qsl(request_body, keep_blank_values=allow_blank)) # type: ignore[type-var] 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 = ( f"request.body doesn't match: {qsl_body} doesn't match {params_dict}" ) return valid, reason return match def json_params_matcher( params: Optional[Union[Mapping[str, Any], List[Any]]], *, strict_match: bool = True ) -> Callable[..., Any]: """Matches JSON encoded data of request body. Parameters ---------- params : dict or list JSON object provided to 'json' arg of request or a part of it if used in conjunction with ``strict_match=False``. strict_match : bool, default=True Applied only when JSON object is a dictionary. If set to ``True``, validates that all keys of JSON object match. If set to ``False``, original request may contain additional keys. Returns ------- Callable Matcher function. """ def match(request: PreparedRequest) -> Tuple[bool, str]: reason = "" request_body = request.body json_params = (params or {}) if not isinstance(params, list) else params try: if isinstance(request.body, bytes): try: request_body = request.body.decode("utf-8") except UnicodeDecodeError: request_body = gzip.decompress(request.body).decode("utf-8") json_body = json_module.loads(request_body) if request_body else {} if ( not strict_match and isinstance(json_body, dict) and isinstance(json_params, dict) ): # filter down to just the params specified in the matcher json_body = _filter_dict_recursively(json_body, json_params) valid = params is None if request_body is None else json_params == json_body if not valid: reason = f"request.body doesn't match: {json_body} doesn't match {json_params}" if not strict_match: reason += ( "\nNote: You use non-strict parameters check, " "to change it use `strict_match=True`." ) 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: Optional[str]) -> Callable[..., Any]: def match(request: PreparedRequest) -> Tuple[bool, str]: reason = "" url_fragment = urlparse(request.url).fragment if identifier: url_fragment_qsl = sorted(parse_qsl(url_fragment)) # type: ignore[type-var] 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: " # type: ignore[str-bytes-safe] f"{identifier} doesn't match {url_fragment}" ) return valid, reason return match def query_param_matcher( params: Optional[MutableMapping[str, Any]], *, strict_match: bool = True ) -> Callable[..., Any]: """Matcher to match 'params' argument in request. Parameters ---------- params : dict The same as provided to request or a part of it if used in conjunction with ``strict_match=False``. strict_match : bool, default=True If set to ``True``, validates that all parameters match. If set to ``False``, original request may contain additional parameters. Returns ------- Callable Matcher function. """ params_dict = params or {} for k, v in params_dict.items(): if isinstance(v, (int, float)): params_dict[k] = str(v) def match(request: PreparedRequest) -> Tuple[bool, str]: reason = "" request_params = request.params # type: ignore[attr-defined] request_params_dict = request_params or {} if not strict_match: # filter down to just the params specified in the matcher request_params_dict = { k: v for k, v in request_params_dict.items() if k in params_dict } valid = sorted(params_dict.items()) == sorted(request_params_dict.items()) if not valid: reason = f"Parameters do not match. {request_params_dict} doesn't match {params_dict}" if not strict_match: reason += ( "\nYou can use `strict_match=True` to do a strict parameters check." ) return valid, reason return match def query_string_matcher(query: Optional[str]) -> Callable[..., Any]: """ Matcher to match query string part of request :param query: (str), same as constructed by request :return: (func) matcher """ def match(request: PreparedRequest) -> Tuple[bool, str]: reason = "" data = parse_url(request.url or "") 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. " f"{dict(request_qsl)} doesn't match {dict(matcher_qsl)}" ) return valid, reason return match def request_kwargs_matcher(kwargs: Optional[Mapping[str, Any]]) -> Callable[..., Any]: """ Matcher to match keyword arguments provided to request :param kwargs: (dict), keyword arguments, same as provided to request :return: (func) matcher """ def match(request: PreparedRequest) -> Tuple[bool, str]: reason = "" kwargs_dict = kwargs or {} # validate only kwargs that were requested for comparison, skip defaults req_kwargs = request.req_kwargs # type: ignore[attr-defined] request_kwargs = {k: v for k, v in req_kwargs.items() if k in kwargs_dict} valid = ( not kwargs_dict if not request_kwargs else sorted(kwargs_dict.items()) == sorted(request_kwargs.items()) ) if not valid: reason = ( f"Arguments don't match: {request_kwargs} doesn't match {kwargs_dict}" ) return valid, reason return match def multipart_matcher( files: Mapping[str, Any], data: Optional[Mapping[str, str]] = None ) -> Callable[..., Any]: """ 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": ""} # type: ignore[assignment] prepared.prepare_body(data=data, files=files) def get_boundary(content_type: str) -> str: """ 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: PreparedRequest) -> Tuple[bool, str]: 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 or "" if isinstance(prepared_body, bytes): # since headers always come as str, need to convert to bytes prepared_boundary = prepared_boundary.encode("utf-8") # type: ignore[assignment] request_boundary = request_boundary.encode("utf-8") # type: ignore[assignment] prepared_body = prepared_body.replace( prepared_boundary, request_boundary # type: ignore[arg-type] ) 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( # type: ignore[str-bytes-safe] request_body, prepared_body ), ) return True, "" return match def header_matcher( headers: Mapping[str, Union[str, Pattern[str]]], strict_match: bool = False ) -> Callable[..., Any]: """ 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 _compare_with_regex(request_headers: Union[Mapping[Any, Any], Any]) -> bool: if strict_match and len(request_headers) != len(headers): return False for k, v in headers.items(): if request_headers.get(k) is not None: if isinstance(v, re.Pattern): if re.match(v, request_headers[k]) is None: return False else: if not v == request_headers[k]: return False else: return False return True def match(request: PreparedRequest) -> Tuple[bool, str]: request_headers: Union[Mapping[Any, Any], Any] = 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 = _compare_with_regex(request_headers) if not valid: return ( False, f"Headers do not match: {request_headers} doesn't match {headers}", ) return valid, "" return match ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1736802180.0 responses-0.25.6/responses/py.typed0000644000175100001660000000023614741277604016765 0ustar00runnerdocker# Marker file for PEP 561. The mypy package uses inline types. # file must be here according to https://peps.python.org/pep-0561/#packaging-type-information ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1736802180.0 responses-0.25.6/responses/registries.py0000644000175100001660000001005514741277604020020 0ustar00runnerdockerimport copy from typing import TYPE_CHECKING from typing import List from typing import Optional from typing import Tuple if TYPE_CHECKING: # pragma: no cover # import only for linter run from requests import PreparedRequest from responses import BaseResponse class FirstMatchRegistry: 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") -> "BaseResponse": if any(response is resp for resp in self.registered): # if user adds multiple responses that reference the same instance. # do a comparison by memory allocation address. # see https://github.com/getsentry/responses/issues/479 response = copy.deepcopy(response) self.registered.append(response) return response def remove(self, response: "BaseResponse") -> List["BaseResponse"]: removed_responses = [] while response in self.registered: self.registered.remove(response) removed_responses.append(response) return removed_responses def replace(self, response: "BaseResponse") -> "BaseResponse": try: index = self.registered.index(response) except ValueError: raise ValueError(f"Response is not registered for URL {response.url}") self.registered[index] = response return response class OrderedRegistry(FirstMatchRegistry): """Registry where `Response` objects are dependent on the insertion order and invocation index. OrderedRegistry applies the rule of first in - first out. Responses should be invoked in the same order in which they were added to the registry. Otherwise, an error is returned. """ def find( self, request: "PreparedRequest" ) -> Tuple[Optional["BaseResponse"], List[str]]: """Find the next registered `Response` and check if it matches the request. Search is performed by taking the first element of the registered responses list and removing this object (popping from the list). Parameters ---------- request : PreparedRequest Request that was caught by the custom adapter. Returns ------- Tuple[Optional["BaseResponse"], List[str]] Matched `Response` object and empty list in case of match. Otherwise, None and a list with reasons for not finding a match. """ if not self.registered: return None, ["No more registered responses"] response = self.registered.pop(0) match_result, reason = response.matches(request) if not match_result: self.reset() self.add(response) reason = ( "Next 'Response' in the order doesn't match " f"due to the following reason: {reason}." ) return None, [reason] return response, [] ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1736802182.8933353 responses-0.25.6/responses/tests/0000755000175100001660000000000014741277607016432 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1736802180.0 responses-0.25.6/responses/tests/__init__.py0000644000175100001660000000000014741277604020526 0ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1736802180.0 responses-0.25.6/responses/tests/test_matchers.py0000644000175100001660000010021014741277604021640 0ustar00runnerdockerimport gzip import re from typing import Any from typing import List from unittest.mock import Mock import pytest import requests from requests.exceptions import ConnectionError import responses from responses import matchers from responses.tests.test_responses import assert_reset from responses.tests.test_responses import assert_response def test_body_match_get(): @responses.activate def run(): url = "http://example.com" responses.add( responses.GET, url, body=b"test", match=[matchers.body_matcher("123456")], ) resp = requests.get("http://example.com", data="123456") assert_response(resp, "test") run() assert_reset() def test_body_match_post(): @responses.activate def run(): url = "http://example.com" responses.add( responses.POST, url, body=b"test", match=[matchers.body_matcher("123456")], ) resp = requests.post("http://example.com", data="123456") assert_response(resp, "test") run() assert_reset() 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_json_params_matcher_not_strict(): @responses.activate(assert_all_requests_are_fired=True) def run(): responses.add( method=responses.POST, url="http://example.com/", body="one", match=[ matchers.json_params_matcher( {"page": {"type": "json"}}, strict_match=False, ) ], ) resp = requests.request( "POST", "http://example.com/", headers={"Content-Type": "application/json"}, json={ "page": {"type": "json", "another": "nested"}, "not_strict": "must pass", }, ) assert_response(resp, "one") run() assert_reset() def test_json_params_matcher_not_strict_diff_values(): @responses.activate def run(): responses.add( method=responses.POST, url="http://example.com/", body="one", match=[ matchers.json_params_matcher( {"page": {"type": "json", "diff": "value"}}, strict_match=False ) ], ) with pytest.raises(ConnectionError) as exc: requests.request( "POST", "http://example.com/", headers={"Content-Type": "application/json"}, json={"page": {"type": "json"}, "not_strict": "must pass"}, ) assert ( "- POST http://example.com/ request.body doesn't match: " "{'page': {'type': 'json'}} doesn't match {'page': {'type': 'json', 'diff': 'value'}}" ) in str(exc.value) run() assert_reset() def test_failed_matchers_dont_modify_inputs_order_in_error_message(): json_a = {"array": ["C", "B", "A"]} json_b = '{"array" : ["B", "A", "C"]}' mock_request = Mock(body=json_b) result = matchers.json_params_matcher(json_a)(mock_request) assert result == ( False, ( "request.body doesn't match: {'array': ['B', 'A', 'C']} " "doesn't match {'array': ['C', 'B', 'A']}" ), ) def test_json_params_matcher_json_list(): json_a = [{"a": "b"}] json_b = '[{"a": "b", "c": "d"}]' mock_request = Mock(body=json_b) result = matchers.json_params_matcher(json_a)(mock_request) assert result == ( False, "request.body doesn't match: [{'a': 'b', 'c': 'd'}] doesn't match [{'a': 'b'}]", ) def test_json_params_matcher_json_list_empty(): json_a: "List[Any]" = [] json_b = "[]" mock_request = Mock(body=json_b) result = matchers.json_params_matcher(json_a)(mock_request) assert result == (True, "") def test_json_params_matcher_body_is_gzipped(): json_a = {"foo": 42, "bar": None} json_b = gzip.compress(b'{"foo": 42, "bar": null}') mock_request = Mock(body=json_b) result = matchers.json_params_matcher(json_a)(mock_request) assert result == (True, "") def test_urlencoded_params_matcher_blank(): @responses.activate def run(): responses.add( method=responses.POST, url="http://example.com/", body="three", match=[ matchers.urlencoded_params_matcher( {"page": "", "type": "urlencoded"}, allow_blank=True ) ], ) resp = requests.request( "POST", "http://example.com/", headers={"Content-Type": "x-www-form-urlencoded"}, data={"page": "", "type": "urlencoded"}, ) assert_response(resp, "three") run() assert_reset() def test_query_params_numbers(): @responses.activate def run(): expected_query_params = {"float": 5.0, "int": 2} responses.add( responses.GET, "https://example.com/", match=[ matchers.query_param_matcher(expected_query_params), ], ) requests.get("https://example.com", params=expected_query_params) run() assert_reset() def test_query_param_matcher_loose(): @responses.activate def run(): expected_query_params = {"only_one_param": "test"} responses.add( responses.GET, "https://example.com/", match=[ matchers.query_param_matcher(expected_query_params, strict_match=False), ], ) requests.get( "https://example.com", params={"only_one_param": "test", "second": "param"} ) run() assert_reset() def test_query_param_matcher_loose_fail(): @responses.activate def run(): expected_query_params = {"does_not_exist": "test"} responses.add( responses.GET, "https://example.com/", match=[ matchers.query_param_matcher(expected_query_params, strict_match=False), ], ) with pytest.raises(ConnectionError) as exc: requests.get( "https://example.com", params={"only_one_param": "test", "second": "param"}, ) assert ( "- GET https://example.com/ Parameters do not match. {} doesn't" " match {'does_not_exist': 'test'}\n" "You can use `strict_match=True` to do a strict parameters check." ) in str(exc.value) run() 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: 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: 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): # type: ignore[misc] @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_header_value_mismatch_raises(): @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_headers_missing_raises(): @responses.activate def run(): url = "http://example.com/" responses.add( method=responses.GET, url=url, json={"success": True}, match=[matchers.header_matcher({"x-custom-header": "foo"})], ) with pytest.raises(ConnectionError) as excinfo: requests.get(url, headers={}) msg = str(excinfo.value) assert ( "Headers do not match: {} doesn't match {'x-custom-header': 'foo'}" ) 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_under_requests_mock_object(): def run(): # ensure all access to responses or matchers is only going # through the RequestsMock instance in the context manager responses = None # noqa: F841 matchers = None # noqa: F841 from responses import RequestsMock with RequestsMock(assert_all_requests_are_fired=True) as rsps: url = "http://example.com" rsps.add( rsps.GET, url, body=b"test", match=[rsps.matchers.body_matcher("123456")], ) resp = requests.get("http://example.com", data="123456") assert_response(resp, "test") run() assert_reset() class TestHeaderWithRegex: @property def url(self): # type: ignore[misc] return "http://example.com/" def _register(self): responses.add( method=responses.GET, url=self.url, body="success", match=[ matchers.header_matcher( { "Accept": "text/plain", "Message-Signature": re.compile(r'signature="\S+",created=\d+'), }, strict_match=True, ) ], ) def test_request_matches_headers_regex(self): @responses.activate def run(): # this one can not use common _register method because different headers responses.add( method=responses.GET, url=self.url, json={"success": True}, match=[ matchers.header_matcher( { "Message-Signature": re.compile( r'signature="\S+",created=\d+' ), "Authorization": "Bearer API_TOKEN", }, strict_match=False, ) ], ) # the actual request can contain extra headers (requests always adds some itself anyway) resp = requests.get( self.url, headers={ "Message-Signature": 'signature="abc",created=1243', "Authorization": "Bearer API_TOKEN", }, ) assert_response( resp, body='{"success": true}', content_type="application/json" ) run() assert_reset() def test_request_matches_headers_regex_strict_match_regex_failed(self): @responses.activate def run(): self._register() session = requests.Session() # requests will add some extra headers of its own, so we have to use prepared requests prepped = session.prepare_request( requests.Request( method="GET", url=self.url, ) ) prepped.headers.clear() prepped.headers["Accept"] = "text/plain" prepped.headers["Message-Signature"] = 'signature="123",created=abc' with pytest.raises(ConnectionError) as excinfo: session.send(prepped) msg = str(excinfo.value) assert ( "Headers do not match: {'Accept': 'text/plain', 'Message-Signature': " """'signature="123",created=abc'} """ "doesn't match {'Accept': 'text/plain', 'Message-Signature': " "re.compile('signature=\"\\\\S+\",created=\\\\d+')}" ) in msg run() assert_reset() def test_request_matches_headers_regex_strict_match_mismatched_field(self): @responses.activate def run(): self._register() # requests will add some extra headers of its own, so we have to use prepared requests session = requests.Session() prepped = session.prepare_request( requests.Request( method="GET", url=self.url, ) ) prepped.headers.clear() prepped.headers["Accept"] = "text/plain" prepped.headers["Accept-Charset"] = "utf-8" # "Accept-Charset" header will fail to match to "Message-Signature" 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', 'Message-Signature': " "re.compile('signature=\"\\\\S+\",created=\\\\d+')}" ) in msg run() assert_reset() def test_request_matches_headers_regex_strict_match_mismatched_number(self): @responses.activate def run(): self._register() # requests will add some extra headers of its own, so we have to use prepared requests session = requests.Session() # include the "Accept-Charset" header, which will fail to match prepped = session.prepare_request( requests.Request( method="GET", url=self.url, ) ) prepped.headers.clear() prepped.headers["Accept"] = "text/plain" prepped.headers["Accept-Charset"] = "utf-8" prepped.headers["Message-Signature"] = 'signature="abc",created=1243' 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', " """'Message-Signature': 'signature="abc",created=1243'} """ "doesn't match {'Accept': 'text/plain', 'Message-Signature': " "re.compile('signature=\"\\\\S+\",created=\\\\d+')}" ) in msg run() assert_reset() def test_request_matches_headers_regex_strict_match_positive(self): @responses.activate def run(): self._register() # requests will add some extra headers of its own, so we have to use prepared requests session = requests.Session() prepped = session.prepare_request( requests.Request( method="GET", url=self.url, ) ) prepped.headers.clear() prepped.headers["Accept"] = "text/plain" prepped.headers["Message-Signature"] = 'signature="abc",created=1243' resp = session.send(prepped) assert_response(resp, body="success", content_type="text/plain") run() assert_reset() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1736802180.0 responses-0.25.6/responses/tests/test_multithreading.py0000644000175100001660000000172114741277604023061 0ustar00runnerdocker""" Separate file for multithreading since it takes time to run """ import threading import pytest import requests import responses @pytest.mark.parametrize("execution_number", range(10)) def test_multithreading_lock(execution_number): # type: ignore[misc] """Reruns test multiple times since error is random and depends on CPU and can lead to false positive result. """ n_threads = 10 n_requests = 30 with responses.RequestsMock() as m: for j in range(n_threads): for i in range(n_requests): m.add(url=f"http://example.com/example{i}", method="GET") def fun(): for req in range(n_requests): requests.get(f"http://example.com/example{req}") threads = [ threading.Thread(name=f"example{i}", target=fun) for i in range(n_threads) ] for thread in threads: thread.start() for thread in threads: thread.join() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1736802180.0 responses-0.25.6/responses/tests/test_recorder.py0000644000175100001660000001716114741277604021653 0ustar00runnerdockerfrom pathlib import Path import pytest import requests import tomli_w import yaml import responses from responses import _recorder from responses._recorder import _dump try: import tomli as _toml except ImportError: # python 3.11+ import tomllib as _toml # type: ignore[no-redef] def get_data(host, port): data = { "responses": [ { "response": { "method": "GET", "url": f"http://{host}:{port}/404", "headers": {"x": "foo"}, "body": "404 Not Found", "status": 404, "content_type": "text/plain", "auto_calculate_content_length": False, } }, { "response": { "method": "GET", "url": f"http://{host}:{port}/status/wrong", "headers": {"x": "foo"}, "body": "Invalid status code", "status": 400, "content_type": "text/plain", "auto_calculate_content_length": False, } }, { "response": { "method": "GET", "url": f"http://{host}:{port}/500", "headers": {"x": "foo"}, "body": "500 Internal Server Error", "status": 500, "content_type": "text/plain", "auto_calculate_content_length": False, } }, { "response": { "method": "PUT", "url": f"http://{host}:{port}/202", "body": "OK", "status": 202, "content_type": "text/plain", "auto_calculate_content_length": False, } }, ] } return data class TestRecord: def setup_method(self): self.out_file = Path("response_record") if self.out_file.exists(): self.out_file.unlink() # pragma: no cover assert not self.out_file.exists() def test_recorder(self, httpserver): url202, url400, url404, url500 = self.prepare_server(httpserver) def another(): requests.get(url500) requests.put(url202) @_recorder.record(file_path=self.out_file) def run(): requests.get(url404) requests.get(url400) another() run() with open(self.out_file) as file: data = yaml.safe_load(file) assert data == get_data(httpserver.host, httpserver.port) def test_recorder_toml(self, httpserver): custom_recorder = _recorder.Recorder() def dump_to_file(file_path, registered): with open(file_path, "wb") as file: _dump(registered, file, tomli_w.dump) # type: ignore[arg-type] custom_recorder.dump_to_file = dump_to_file # type: ignore[assignment] url202, url400, url404, url500 = self.prepare_server(httpserver) def another(): requests.get(url500) requests.put(url202) @custom_recorder.record(file_path=self.out_file) def run(): requests.get(url404) requests.get(url400) another() run() with open(self.out_file, "rb") as file: data = _toml.load(file) assert data == get_data(httpserver.host, httpserver.port) def prepare_server(self, httpserver): httpserver.expect_request("/500").respond_with_data( "500 Internal Server Error", status=500, content_type="text/plain", headers={"x": "foo"}, ) httpserver.expect_request("/202").respond_with_data( "OK", status=202, content_type="text/plain", ) httpserver.expect_request("/404").respond_with_data( "404 Not Found", status=404, content_type="text/plain", headers={"x": "foo"}, ) httpserver.expect_request("/status/wrong").respond_with_data( "Invalid status code", status=400, content_type="text/plain", headers={"x": "foo"}, ) url500 = httpserver.url_for("/500") url202 = httpserver.url_for("/202") url404 = httpserver.url_for("/404") url400 = httpserver.url_for("/status/wrong") return url202, url400, url404, url500 def test_use_recorder_without_decorator(self, httpserver): """I want to be able to record in the REPL.""" url202, url400, url404, url500 = self.prepare_server(httpserver) _recorder.recorder.start() def another(): requests.get(url500) requests.put(url202) def run(): requests.get(url404) requests.get(url400) another() run() _recorder.recorder.stop() _recorder.recorder.dump_to_file(self.out_file) with open(self.out_file) as file: data = yaml.safe_load(file) assert data == get_data(httpserver.host, httpserver.port) # Now, we test that the recorder is properly reset assert _recorder.recorder.get_registry().registered _recorder.recorder.reset() assert not _recorder.recorder.get_registry().registered class TestReplay: def setup_method(self): self.out_file = Path("response_record") def teardown_method(self): if self.out_file.exists(): self.out_file.unlink() assert not self.out_file.exists() @pytest.mark.parametrize("parser", (yaml, tomli_w)) def test_add_from_file(self, parser): # type: ignore[misc] if parser == yaml: with open(self.out_file, "w") as file: parser.dump(get_data("example.com", "8080"), file) else: with open(self.out_file, "wb") as file: # type: ignore[assignment] parser.dump(get_data("example.com", "8080"), file) @responses.activate def run(): responses.patch("http://httpbin.org") if parser == tomli_w: def _parse_resp_f(file_path): with open(file_path, "rb") as file: data = _toml.load(file) return data responses.mock._parse_response_file = _parse_resp_f # type: ignore[method-assign] responses._add_from_file(file_path=self.out_file) responses.post("http://httpbin.org/form") assert responses.registered()[0].url == "http://httpbin.org/" assert responses.registered()[1].url == "http://example.com:8080/404" assert ( responses.registered()[2].url == "http://example.com:8080/status/wrong" ) assert responses.registered()[3].url == "http://example.com:8080/500" assert responses.registered()[4].url == "http://example.com:8080/202" assert responses.registered()[5].url == "http://httpbin.org/form" assert responses.registered()[0].method == "PATCH" assert responses.registered()[2].method == "GET" assert responses.registered()[4].method == "PUT" assert responses.registered()[5].method == "POST" assert responses.registered()[2].status == 400 assert responses.registered()[3].status == 500 assert responses.registered()[3].body == "500 Internal Server Error" assert responses.registered()[3].content_type == "text/plain" run() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1736802180.0 responses-0.25.6/responses/tests/test_registries.py0000644000175100001660000001326314741277604022225 0ustar00runnerdockerimport pytest import requests from requests.exceptions import ConnectionError import responses from responses import registries from responses.registries import OrderedRegistry from responses.tests.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_reversed(): """See https://github.com/getsentry/responses/issues/563""" class CustomRegistry(registries.FirstMatchRegistry): pass @responses.activate def run(): # test that registry does not leak to another test assert type(responses.mock.get_registry()) == registries.FirstMatchRegistry @responses.activate(registry=CustomRegistry) def run_with_registry(): assert type(responses.mock.get_registry()) == CustomRegistry run() run_with_registry() assert_reset() @pytest.mark.asyncio async def test_registry_async(): # type: ignore[misc] class CustomRegistry(registries.FirstMatchRegistry): pass @responses.activate async def run(): # test that registry does not leak to another test assert type(responses.mock.get_registry()) == registries.FirstMatchRegistry @responses.activate(registry=CustomRegistry) async def run_with_registry(): assert type(responses.mock.get_registry()) == CustomRegistry await run() await run_with_registry() 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() class TestOrderedRegistry: def test_invocation_index(self): @responses.activate(registry=OrderedRegistry) def run(): responses.add( responses.GET, "http://twitter.com/api/1/foobar", status=666, ) responses.add( responses.GET, "http://twitter.com/api/1/foobar", status=667, ) responses.add( responses.GET, "http://twitter.com/api/1/foobar", status=668, ) responses.add( responses.GET, "http://twitter.com/api/1/foobar", status=669, ) resp = requests.get("http://twitter.com/api/1/foobar") assert resp.status_code == 666 resp = requests.get("http://twitter.com/api/1/foobar") assert resp.status_code == 667 resp = requests.get("http://twitter.com/api/1/foobar") assert resp.status_code == 668 resp = requests.get("http://twitter.com/api/1/foobar") assert resp.status_code == 669 run() assert_reset() def test_not_match(self): @responses.activate(registry=OrderedRegistry) def run(): responses.add( responses.GET, "http://twitter.com/api/1/foobar", json={"msg": "not found"}, status=667, ) responses.add( responses.GET, "http://twitter.com/api/1/barfoo", json={"msg": "not found"}, status=404, ) responses.add( responses.GET, "http://twitter.com/api/1/foobar", json={"msg": "OK"}, status=200, ) resp = requests.get("http://twitter.com/api/1/foobar") assert resp.status_code == 667 with pytest.raises(ConnectionError) as excinfo: requests.get("http://twitter.com/api/1/foobar") msg = str(excinfo.value) assert ( "- GET http://twitter.com/api/1/barfoo Next 'Response' in the " "order doesn't match due to the following reason: URL does not match" ) in msg run() assert_reset() def test_empty_registry(self): @responses.activate(registry=OrderedRegistry) def run(): with pytest.raises(ConnectionError): requests.get("http://twitter.com/api/1/foobar") run() assert_reset() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1736802180.0 responses-0.25.6/responses/tests/test_responses.py0000644000175100001660000024571014741277604022072 0ustar00runnerdockerimport inspect import os import re import tempfile import warnings from io import BufferedReader from io import BytesIO from typing import Any from typing import List from typing import Optional from unittest.mock import Mock from unittest.mock import patch import pytest import requests import urllib3 from requests.exceptions import ChunkedEncodingError from requests.exceptions import ConnectionError from requests.exceptions import HTTPError from requests.exceptions import RetryError from urllib3.util.retry import Retry import responses from responses import BaseResponse from responses import Call from responses import CallbackResponse from responses import PassthroughResponse from responses import Response from responses import matchers from responses import registries def assert_reset(): assert len(responses.mock.registered()) == 0 assert len(responses.calls) == 0 def assert_response( resp: Any, body: Optional[Any] = None, content_type: "Optional[str]" = "text/plain" ) -> None: 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() def test_response_with_instance_under_requests_mock_object(): def run(): # ensure all access to responses is only going through # the RequestsMock instance in the context manager responses = None # noqa: F841 from responses import RequestsMock with RequestsMock(assert_all_requests_are_fired=True) as rsps: rsps.add(rsps.Response(method=rsps.GET, url="http://example.com")) resp = requests.get("http://example.com") assert_response(resp, "") assert len(rsps.calls) == 1 assert rsps.calls[0].request.url == "http://example.com/" resp = requests.get("http://example.com?foo=bar") assert_response(resp, "") assert len(rsps.calls) == 2 assert rsps.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): # type: ignore[misc] @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): # type: ignore[misc] @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): # type: ignore[misc] @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): # type: ignore[misc] @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): # type: ignore[misc] 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_deprecated_package_attributes(): """Validates that deprecation warning is raised when package attributes are called.""" # keep separate context manager to avoid leakage with pytest.deprecated_call(): responses.assert_all_requests_are_fired with pytest.deprecated_call(): responses.passthru_prefixes with pytest.deprecated_call(): responses.target 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 warnings.catch_warnings(): warnings.simplefilter("error") run() assert_reset() def test_callback_exception_result(): result = Exception() url = "http://example.com/" def request_callback(_request): return result @responses.activate def run(): responses.add_callback(responses.GET, url, request_callback) with pytest.raises(Exception) as e: requests.get(url) assert e.value is result run() assert_reset() def test_callback_exception_body(): body = Exception() url = "http://example.com/" def request_callback(_request): return 200, {}, body @responses.activate def run(): responses.add_callback(responses.GET, url, request_callback) with pytest.raises(Exception) as e: requests.get(url) assert e.value is body run() assert_reset() def test_callback_no_content_type(): body = b"test callback" status = 400 reason = "Bad Request" headers = {"foo": "bar"} url = "http://example.com/" def request_callback(_request): return status, headers, body @responses.activate def run(): responses.add_callback(responses.GET, url, request_callback, content_type=None) resp = requests.get(url) assert resp.text == "test callback" assert resp.status_code == status assert resp.reason == reason assert "foo" in resp.headers assert "Content-Type" not in resp.headers run() assert_reset() def test_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()) class TestAdapters: class CustomAdapter(requests.adapters.HTTPAdapter): """Classic custom adapter.""" def send(self, *a, **k): return super().send(*a, **k) class PositionalArgsAdapter(requests.adapters.HTTPAdapter): """Custom adapter that sends only positional args. See https://github.com/getsentry/responses/issues/642 for more into. """ def send( self, request, stream=False, timeout=None, verify=True, cert=None, proxies=None, ): return super().send(request, stream, timeout, verify, cert, proxies) class PositionalArgsIncompleteAdapter(requests.adapters.HTTPAdapter): """Custom adapter that sends only positional args. Not all arguments are forwarded to the send method. See https://github.com/getsentry/responses/issues/642 for more into. """ def send( self, request, stream=False, timeout=None, verify=True, # following args are intentionally not forwarded cert=None, proxies=None, ): return super().send(request, stream, timeout, verify) @pytest.mark.parametrize( "adapter_class", (CustomAdapter, PositionalArgsAdapter, PositionalArgsIncompleteAdapter), ) def test_custom_adapter(self, adapter_class): # type: ignore[misc] """Test basic adapter implementation and that responses can patch them properly.""" @responses.activate def run(): url = "http://example.com" responses.add(responses.GET, url, body=b"test adapter") # Test that the adapter is actually used session = requests.Session() adapter = adapter_class() session.mount("http://", adapter) with patch.object(adapter, "send", side_effect=adapter.send) as mock_send: resp = session.get(url, allow_redirects=False) assert mock_send.call_count == 1 assert_response(resp, "test adapter") 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(): # type: ignore[misc] return "apple" @pytest.fixture def fruit_basket(my_fruit): # type: ignore[misc] return ["banana", my_fruit] @pytest.mark.usefixtures("my_fruit", "fruit_basket") class TestFixtures: """ 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): # type: ignore[misc] 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: 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()) == {"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()) == {"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()) == {"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): # type: ignore[misc] @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()) == {"mycookie"} assert "mycookie" in session.cookies assert session.cookies["mycookie"] == "cookieval" assert set(session.cookies.keys()) == {"mycookie"} run() assert_reset() def test_response_callback(): """adds a callback to decorate the response, then checks it""" def run(): def response_callback(response): response._is_mocked = True return response 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, encoding="utf-8") 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, encoding="utf-8") 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_assert_all_requests_fired_multiple(): @responses.activate(assert_all_requests_are_fired=True) def test_some_function(): # Not all mocks are called so we'll get an AssertionError responses.add(responses.GET, "http://other_url", json={}) responses.add(responses.GET, "http://some_api", json={}) requests.get("http://some_api") @responses.activate(assert_all_requests_are_fired=True) def test_some_second_function(): # This should pass as mocks should be reset. responses.add(responses.GET, "http://some_api", json={}) requests.get("http://some_api") with pytest.raises(AssertionError): test_some_function() assert_reset() test_some_second_function() assert_reset() def test_allow_redirects_samehost(): redirecting_url = "http://example.com" final_url_path = "/1" final_url = f"{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": f"/{n!s}"} 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_path_segments(): """Test that path segment after ``;`` is preserved. Validate compliance with RFC 3986. The path is terminated by the first question mark ("?") or number sign ("#") character, or by the end of the URI. See more about how path should be treated under: https://datatracker.ietf.org/doc/html/rfc3986.html#section-3.3 """ @responses.activate def run(): responses.add(responses.GET, "http://example.com/here/we", status=669) responses.add(responses.GET, "http://example.com/here/we;go", status=777) resp = requests.get("http://example.com/here/we;go") assert resp.status_code == 777 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_headers_deduplicated_content_type(): """Test to ensure that we do not have two values for `content-type`. For more details see https://github.com/getsentry/responses/issues/644 """ @responses.activate def run(): responses.get( "https://example.org/", json={}, headers={"Content-Type": "application/json"}, ) responses.start() resp = requests.get("https://example.org/") assert resp.headers["Content-Type"] == "application/json" 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(urllib3.HTTPResponse, "__init__") def patched_init(self, *args, **kwargs): kwargs["enforce_content_length"] = True original_init(self, *args, **kwargs) monkeypatch.setattr(urllib3.HTTPResponse, "__init__", patched_init) 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_legacy_adding_headers_with_content_type(): @responses.activate def run(): with pytest.raises(RuntimeError) as excinfo: responses.add( responses.GET, "http://example.com", body="test", content_type="text/html", adding_headers={"Content-Type": "text/html; charset=utf-8"}, ) assert ( "You cannot define both `content_type` and `headers[Content-Type]`" in str(excinfo.value) ) 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, ) if urllib3.__version__ < "2": resp = requests.get(url) assert_response(resp, "test") assert resp.headers["Content-Length"] == "2" else: with pytest.raises(ChunkedEncodingError) as excinfo: requests.get(url) assert "IncompleteRead(4 bytes read, -2 more expected)" in str( excinfo.value ) 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() class TestPassthru: def test_passthrough_flag(self, httpserver): httpserver.expect_request("/").respond_with_data( "OK", content_type="text/plain" ) url = httpserver.url_for("/") response = Response(responses.GET, url, body="MOCK") @responses.activate def run_passthrough(): responses.add(response) resp = requests.get(url) assert_response(resp, "OK") @responses.activate def run_mocked(): responses.add(response) resp = requests.get(url) assert_response(resp, "MOCK") run_mocked() assert_reset() response.passthrough = True run_passthrough() assert_reset() def test_passthrough_kwarg(self, httpserver): httpserver.expect_request("/").respond_with_data( "OK", content_type="text/plain" ) url = httpserver.url_for("/") def configure_response(passthrough): responses.get(url, body="MOCK", passthrough=passthrough) @responses.activate def run_passthrough(): configure_response(passthrough=True) resp = requests.get(url) assert_response(resp, "OK") @responses.activate def run_mocked(): configure_response(passthrough=False) resp = requests.get(url) assert_response(resp, "MOCK") run_mocked() assert_reset() run_passthrough() assert_reset() def test_passthrough_response(self, httpserver): httpserver.expect_request("/").respond_with_data( "OK", content_type="text/plain" ) url = httpserver.url_for("/") @responses.activate def run(): responses.add(PassthroughResponse(responses.GET, url)) responses.add(responses.GET, f"{url}/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(f"{url}/one") assert_response(resp, "one") resp = requests.get(url) assert_response(resp, "OK") assert len(responses.calls) == 3 responses.assert_call_count(url, 1) run() assert_reset() def test_passthrough_response_stream(self, httpserver): httpserver.expect_request("/").respond_with_data( "OK", content_type="text/plain" ) @responses.activate def run(): url = httpserver.url_for("/") responses.add(PassthroughResponse(responses.GET, url)) content_1 = requests.get(url).content with requests.get(url, stream=True) as resp: content_2 = resp.raw.read() assert content_1 == content_2 run() assert_reset() def test_passthru_prefixes(self, httpserver): httpserver.expect_request("/").respond_with_data( "OK", content_type="text/plain" ) url = httpserver.url_for("/") @responses.activate def run_constructor_argument(): with responses.RequestsMock(passthru_prefixes=(url,)): resp = requests.get(url) assert_response(resp, "OK") @responses.activate def run_property_setter(): with responses.RequestsMock() as m: m.passthru_prefixes = tuple([url]) resp = requests.get(url) assert_response(resp, "OK") run_constructor_argument() assert_reset() run_property_setter() assert_reset() def test_passthru(self, httpserver): httpserver.expect_request("/").respond_with_data( "OK", content_type="text/plain" ) url = httpserver.url_for("/") @responses.activate def run(): responses.add_passthru(url) responses.add(responses.GET, f"{url}/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(f"{url}/one") assert_response(resp, "one") resp = requests.get(url) assert_response(resp, "OK") run() assert_reset() def test_passthru_regex(self, httpserver): httpserver.expect_request(re.compile("^/\\w+")).respond_with_data( "OK", content_type="text/plain" ) url = httpserver.url_for("/") @responses.activate def run(): responses.add_passthru(re.compile(f"{url}/\\w+")) responses.add(responses.GET, f"{url}/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(f"{url}/one") assert_response(resp, "one") resp = requests.get(f"{url}/two") assert_response(resp, "OK") resp = requests.get(f"{url}/three") assert_response(resp, "OK") run() assert_reset() def test_passthru_does_not_persist_across_tests(self, httpserver): """ passthru should be erased on exit from context manager see: https://github.com/getsentry/responses/issues/322 """ httpserver.expect_request("/").respond_with_data( "mocked server", status=969, content_type="text/plain" ) @responses.activate def with_a_passthru(): assert not responses.mock.passthru_prefixes responses.add_passthru(re.compile(".*")) url = httpserver.url_for("/") response = requests.get(url) assert response.status_code == 969 assert response.text == "mocked server" @responses.activate def without_a_passthru(): assert not responses.mock.passthru_prefixes with pytest.raises(requests.exceptions.ConnectionError): requests.get("https://example.com") with_a_passthru() without_a_passthru() def test_passthru_unicode(self): @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_real_send_argument(self): def run(): # the following mock will serve to catch the real send request from another mock and # will "donate" `unbound_on_send` method mock_to_catch_real_send = responses.RequestsMock( assert_all_requests_are_fired=True ) mock_to_catch_real_send.post( "http://send-this-request-through.com", status=500 ) with responses.RequestsMock( assert_all_requests_are_fired=True, real_adapter_send=mock_to_catch_real_send.unbound_on_send(), ) as r_mock: r_mock.add_passthru("http://send-this-request-through.com") r_mock.add(responses.POST, "https://example.org", status=200) response = requests.post("https://example.org") assert response.status_code == 200 response = requests.post("http://send-this-request-through.com") assert response.status_code == 500 run() assert_reset() def test_method_named_param(): @responses.activate def run(): responses.add(method=responses.GET, url="http://example.com", body="OK") resp = requests.get("http://example.com") assert_response(resp, "OK") run() assert_reset() def test_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" @pytest.mark.parametrize( "url", ( "http://example.com", "http://example.com/some/path", "http://example.com/other/path/", ), ) def test_request_param(url): # type: ignore[misc] @responses.activate def run(): params = {"hello": "world", "example": "params"} responses.add( method=responses.GET, url=f"{url}?hello=world", 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): # type: ignore[misc] @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 '{}' 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 '{}' to be called 3 times. Called 2 times.".format( url ) in str(excinfo.value) run() assert_reset() def test_call_count_with_matcher(): @responses.activate def run(): rsp = responses.add( responses.GET, "http://www.example.com", match=(matchers.query_param_matcher({}),), ) rsp2 = responses.add( responses.GET, "http://www.example.com", match=(matchers.query_param_matcher({"hello": "world"}),), status=777, ) requests.get("http://www.example.com") resp1 = requests.get("http://www.example.com") requests.get("http://www.example.com?hello=world") resp2 = requests.get("http://www.example.com?hello=world") assert resp1.status_code == 200 assert resp2.status_code == 777 assert rsp.call_count == 2 assert rsp2.call_count == 2 run() assert_reset() def test_call_count_without_matcher(): @responses.activate def run(): rsp = responses.add(responses.GET, "http://www.example.com") requests.get("http://www.example.com") requests.get("http://www.example.com") requests.get("http://www.example.com?hello=world") requests.get("http://www.example.com?hello=world") assert rsp.call_count == 4 run() assert_reset() def test_response_calls_indexing_and_slicing(): @responses.activate def run(): responses.add(responses.GET, "http://www.example.com") responses.add(responses.GET, "http://www.example.com/1") responses.add(responses.GET, "http://www.example.com/2") requests.get("http://www.example.com") requests.get("http://www.example.com/1") requests.get("http://www.example.com/2") requests.get("http://www.example.com/1") requests.get("http://www.example.com") # Use of a type hints here ensures mypy knows the difference between index and slice. individual_call: Call = responses.calls[0] call_slice: List[Call] = responses.calls[1:-1] assert individual_call.request.url == "http://www.example.com/" assert call_slice == [ responses.calls[1], responses.calls[2], responses.calls[3], ] assert [c.request.url for c in call_slice] == [ "http://www.example.com/1", "http://www.example.com/2", "http://www.example.com/1", ] run() assert_reset() def test_response_calls_and_registry_calls_are_equal(): @responses.activate def run(): rsp1 = responses.add(responses.GET, "http://www.example.com") rsp2 = responses.add(responses.GET, "http://www.example.com/1") rsp3 = responses.add( responses.GET, "http://www.example.com/2" ) # won't be requested requests.get("http://www.example.com") requests.get("http://www.example.com/1") requests.get("http://www.example.com") assert len(responses.calls) == len(rsp1.calls) + len(rsp2.calls) + len( rsp3.calls ) assert rsp1.call_count == 2 assert len(rsp1.calls) == 2 assert rsp1.calls[0] is responses.calls[0] assert rsp1.calls[1] is responses.calls[2] assert rsp2.call_count == 1 assert len(rsp2.calls) == 1 assert rsp2.calls[0] is responses.calls[1] assert rsp3.call_count == 0 assert len(rsp3.calls) == 0 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") rsps.add_passthru("http://other.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 assert "Passthru prefixes:\n- http://other.example.com" 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): # type: ignore[misc] 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): # type: ignore[misc] @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() def test_responses_reuse(): @responses.activate def run(): url = "https://someapi.com/" fail_response = responses.Response( method="GET", url=url, body="fail", status=500 ) responses.add(responses.GET, url, "success", status=200) responses.add(fail_response) responses.add(fail_response) responses.add(fail_response) responses.add(responses.GET, url, "success", status=200) responses.add(responses.GET, url, "", status=302) response = requests.get(url) assert response.content == b"success" for _ in range(3): response = requests.get(url) assert response.content == b"fail" run() assert_reset() @pytest.mark.asyncio async def test_async_calls(): # type: ignore[misc] @responses.activate async def run(): 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 responses.calls[0].request.url == "http://twitter.com/api/1/foobar" await run() assert_reset() class TestStrictWrapper: def test_strict_wrapper(self): """Test that assert_all_requests_are_fired could be applied to the decorator.""" @responses.activate(assert_all_requests_are_fired=True) def run_strict(): responses.add(responses.GET, "https://someapi1.com/", "success") responses.add(responses.GET, "https://notcalled1.com/", "success") requests.get("https://someapi1.com/") assert responses.mock.assert_all_requests_are_fired @responses.activate(assert_all_requests_are_fired=False) def run_not_strict(): responses.add(responses.GET, "https://someapi2.com/", "success") responses.add(responses.GET, "https://notcalled2.com/", "success") requests.get("https://someapi2.com/") assert not responses.mock.assert_all_requests_are_fired @responses.activate def run_classic(): responses.add(responses.GET, "https://someapi3.com/", "success") responses.add(responses.GET, "https://notcalled3.com/", "success") requests.get("https://someapi3.com/") assert not responses.mock.assert_all_requests_are_fired # keep the order of function calls to ensure that decorator doesn't leak to another function with pytest.raises(AssertionError) as exc_info: run_strict() # check that one URL is in uncalled assertion assert "https://notcalled1.com/" in str(exc_info.value) run_classic() run_not_strict() @pytest.mark.parametrize("assert_fired", (True, False, None)) def test_nested_decorators(self, assert_fired): # type: ignore[misc] """Validate if assert_all_requests_are_fired is applied from the correct function. assert_all_requests_are_fired must be applied from the function where call to 'requests' is done. Test matrix of True/False/None values applied to validate different scenarios. """ @responses.activate(assert_all_requests_are_fired=assert_fired) def wrapped(): responses.add(responses.GET, "https://notcalled1.com/", "success") responses.add(responses.GET, "http://example.com/1", body="Hello 1") assert b"Hello 1" == requests.get("http://example.com/1").content @responses.activate(assert_all_requests_are_fired=not assert_fired) def call_another_wrapped_function(): responses.add(responses.GET, "https://notcalled2.com/", "success") wrapped() if assert_fired: with pytest.raises(AssertionError) as exc_info: call_another_wrapped_function() assert "https://notcalled1.com/" in str(exc_info.value) assert "https://notcalled2.com/" in str(exc_info.value) else: call_another_wrapped_function() class TestMultipleWrappers: """Test to validate that multiple decorators could be applied. Ensures that we can call one function that is wrapped with ``responses.activate`` decorator from within another wrapped function. Validates that mock patch is not leaked to other tests. For more detail refer to https://github.com/getsentry/responses/issues/481 """ @responses.activate def test_wrapped(self): responses.add(responses.GET, "http://example.com/1", body="Hello 1") assert b"Hello 1" == requests.get("http://example.com/1").content @responses.activate def test_call_another_wrapped_function(self): self.test_wrapped() def test_mock_not_leaked(self, httpserver): """ Validate that ``responses.activate`` does not leak to unpatched test. Parameters ---------- httpserver : ContentServer Mock real HTTP server """ httpserver.expect_request("/").respond_with_data( "OK", content_type="text/plain", status=969 ) url = httpserver.url_for("/") response = requests.get(url) assert response.status_code == 969 class TestShortcuts: def test_delete(self): @responses.activate def run(): responses.delete("http://example.com/1", status=888) resp = requests.delete("http://example.com/1") assert resp.status_code == 888 run() assert_reset() def test_get(self): @responses.activate def run(): responses.get("http://example.com/1", status=888) resp = requests.get("http://example.com/1") assert resp.status_code == 888 run() assert_reset() def test_head(self): @responses.activate def run(): responses.head("http://example.com/1", status=888) resp = requests.head("http://example.com/1") assert resp.status_code == 888 run() assert_reset() def test_head_with_content_length(self): @responses.activate def run(): headers = {"content-length": "1000"} responses.head("http://example.com/1", status=200, headers=headers) resp = requests.head("http://example.com/1") assert resp.status_code == 200 assert resp.headers["Content-Length"] == "1000" run() assert_reset() def test_options(self): @responses.activate def run(): responses.options("http://example.com/1", status=888) resp = requests.options("http://example.com/1") assert resp.status_code == 888 run() assert_reset() def test_patch(self): @responses.activate def run(): responses.patch("http://example.com/1", status=888) resp = requests.patch("http://example.com/1") assert resp.status_code == 888 run() assert_reset() def test_post(self): @responses.activate def run(): responses.post("http://example.com/1", status=888) resp = requests.post("http://example.com/1") assert resp.status_code == 888 run() assert_reset() def test_put(self): @responses.activate def run(): responses.put("http://example.com/1", status=888) resp = requests.put("http://example.com/1") assert resp.status_code == 888 run() assert_reset() class TestUnitTestPatchSetup: """Validates that ``RequestsMock`` could be used as ``mock.patch``. This class is present as example in README.rst """ def setup_method(self): self.r_mock = responses.RequestsMock(assert_all_requests_are_fired=True) self.r_mock.start() self.r_mock.get("https://example.com", status=505) self.r_mock.put("https://example.com", status=506) def teardown_method(self): self.r_mock.stop() self.r_mock.reset() assert_reset() def test_function(self): resp = requests.get("https://example.com") assert resp.status_code == 505 resp = requests.put("https://example.com") assert resp.status_code == 506 class TestUnitTestPatchSetupRaises: """Validate that teardown raises if not all requests were executed. Similar to ``TestUnitTestPatchSetup``. """ def setup_method(self): self.r_mock = responses.RequestsMock() self.r_mock.start() self.r_mock.get("https://example.com", status=505) self.r_mock.put("https://example.com", status=506) def teardown_method(self): with pytest.raises(AssertionError) as exc: self.r_mock.stop() self.r_mock.reset() assert "[('PUT', 'https://example.com/')]" in str(exc.value) assert_reset() def test_function(self): resp = requests.get("https://example.com") assert resp.status_code == 505 def test_reset_in_the_middle(): @responses.activate def run(): with responses.RequestsMock() as rsps2: rsps2.reset() responses.add(responses.GET, "https://example.invalid", status=200) resp = requests.request("GET", "https://example.invalid") assert resp.status_code == 200 run() assert_reset() def test_redirect(): @responses.activate def run(): # create multiple Response objects where first two contain redirect headers rsp1 = responses.Response( responses.GET, "http://example.com/1", status=301, headers={"Location": "http://example.com/2"}, ) rsp2 = responses.Response( responses.GET, "http://example.com/2", status=301, headers={"Location": "http://example.com/3"}, ) rsp3 = responses.Response(responses.GET, "http://example.com/3", status=200) # register above generated Responses in `response` module responses.add(rsp1) responses.add(rsp2) responses.add(rsp3) # do the first request in order to generate genuine `requests` response # this object will contain genuine attributes of the response, like `history` rsp = requests.get("http://example.com/1") responses.calls.reset() # customize exception with `response` attribute my_error = requests.ConnectionError("custom error") my_error.response = rsp # update body of the 3rd response with Exception, this will be raised during execution rsp3.body = my_error with pytest.raises(requests.ConnectionError) as exc_info: requests.get("http://example.com/1") assert exc_info.value.args[0] == "custom error" assert rsp1.url in exc_info.value.response.history[0].url assert rsp2.url in exc_info.value.response.history[1].url run() assert_reset() class TestMaxRetry: def set_session(self, total=4, raise_on_status=True): session = requests.Session() adapter = requests.adapters.HTTPAdapter( max_retries=Retry( total=total, backoff_factor=0.1, status_forcelist=[500], allowed_methods=["GET", "POST", "PATCH"], raise_on_status=raise_on_status, ) ) session.mount("https://", adapter) return session def test_max_retries(self): """This example is present in README.rst""" @responses.activate(registry=registries.OrderedRegistry) def run(): url = "https://example.com" rsp1 = responses.get(url, body="Error", status=500) rsp2 = responses.get(url, body="Error", status=500) rsp3 = responses.get(url, body="Error", status=500) rsp4 = responses.get(url, body="OK", status=200) session = self.set_session() resp = session.get(url) assert resp.status_code == 200 assert rsp1.call_count == 1 assert rsp2.call_count == 1 assert rsp3.call_count == 1 assert rsp4.call_count == 1 run() assert_reset() @pytest.mark.parametrize("raise_on_status", (True, False)) def test_max_retries_exceed(self, raise_on_status): # type: ignore[misc] @responses.activate(registry=registries.OrderedRegistry) def run(): url = "https://example.com" rsp1 = responses.get(url, body="Error", status=500) rsp2 = responses.get(url, body="Error", status=500) rsp3 = responses.get(url, body="Error", status=500) session = self.set_session(total=2, raise_on_status=raise_on_status) if raise_on_status: with pytest.raises(RetryError): session.get(url) else: resp = session.get(url) assert resp.status_code == 500 assert rsp1.call_count == 1 assert rsp2.call_count == 1 assert rsp3.call_count == 1 run() assert_reset() def test_max_retries_exceed_msg(self): @responses.activate(registry=registries.OrderedRegistry) def run(): url = "https://example.com" responses.get(url, body="Error", status=500) responses.get(url, body="Error", status=500) session = self.set_session(total=1) with pytest.raises(RetryError) as err: session.get(url) assert "too many 500 error responses" in str(err.value) run() assert_reset() def test_adapter_retry_untouched(self): """Validate that every new request uses brand-new Retry object""" @responses.activate(registry=registries.OrderedRegistry) def run(): url = "https://example.com" error_rsp = responses.get(url, body="Error", status=500) responses.add(error_rsp) responses.add(error_rsp) ok_rsp = responses.get(url, body="OK", status=200) responses.add(error_rsp) responses.add(error_rsp) responses.add(error_rsp) responses.add(ok_rsp) session = self.set_session() resp = session.get(url) assert resp.status_code == 200 resp = session.get(url) assert resp.status_code == 200 assert len(responses.calls) == 8 run() assert_reset() def test_request_object_attached_to_exception(): """Validate that we attach `request` object to custom exception supplied as body""" @responses.activate def run(): url = "https://httpbin.org/delay/2" responses.get(url, body=requests.ReadTimeout()) try: requests.get(url, timeout=1) except requests.ReadTimeout as exc: assert type(exc.request) == requests.models.PreparedRequest run() assert_reset() def test_file_like_body_in_request(): """Validate that when file-like objects are used in requests the data can be accessed in the call list. This ensures that we are not storing file handles that may be closed by the time the user wants to assert on the data in the request. GH #719. """ @responses.activate def run(): responses.add(responses.POST, "https://example.com") with tempfile.TemporaryFile() as f: f.write(b"test") f.seek(0) requests.post("https://example.com", data=f) assert len(responses.calls) == 1 assert responses.calls[0].request.body == b"test" run() assert_reset() ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1736802182.8923354 responses-0.25.6/responses.egg-info/0000755000175100001660000000000014741277607016762 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1736802182.0 responses-0.25.6/responses.egg-info/PKG-INFO0000644000175100001660000013267114741277606020070 0ustar00runnerdockerMetadata-Version: 2.1 Name: responses Version: 0.25.6 Summary: A utility library for mocking out the `requests` Python library. Home-page: https://github.com/getsentry/responses Author: David Cramer License: Apache 2.0 Project-URL: Bug Tracker, https://github.com/getsentry/responses/issues Project-URL: Changes, https://github.com/getsentry/responses/blob/master/CHANGES Project-URL: Documentation, https://github.com/getsentry/responses/blob/master/README.rst Project-URL: Source Code, https://github.com/getsentry/responses 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.8 Classifier: Programming Language :: Python :: 3.9 Classifier: Programming Language :: Python :: 3.10 Classifier: Programming Language :: Python :: 3.11 Classifier: Programming Language :: Python :: 3.12 Classifier: Topic :: Software Development Requires-Python: >=3.8 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://img.shields.io/pypi/dm/responses :target: https://pypi.python.org/pypi/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.8 or newer, and requests >= 2.30.0 Table of Contents ----------------- .. contents:: Installing ---------- ``pip install responses`` Deprecations and Migration Path ------------------------------- Here you will find a list of deprecated functionality and a migration path for each. Please ensure to update your code according to the guidance. .. list-table:: Deprecation and Migration :widths: 50 25 50 :header-rows: 1 * - Deprecated Functionality - Deprecated in Version - Migration Path * - ``responses.json_params_matcher`` - 0.14.0 - ``responses.matchers.json_params_matcher`` * - ``responses.urlencoded_params_matcher`` - 0.14.0 - ``responses.matchers.urlencoded_params_matcher`` * - ``stream`` argument in ``Response`` and ``CallbackResponse`` - 0.15.0 - Use ``stream`` argument in request directly. * - ``match_querystring`` argument in ``Response`` and ``CallbackResponse``. - 0.17.0 - Use ``responses.matchers.query_param_matcher`` or ``responses.matchers.query_string_matcher`` * - ``responses.assert_all_requests_are_fired``, ``responses.passthru_prefixes``, ``responses.target`` - 0.20.0 - Use ``responses.mock.assert_all_requests_are_fired``, ``responses.mock.passthru_prefixes``, ``responses.mock.target`` instead. Basics ------ The core of ``responses`` comes from registering mock responses and covering test function with ``responses.activate`` decorator. ``responses`` provides similar interface as ``requests``. Main Interface ^^^^^^^^^^^^^^ * responses.add(``Response`` or ``Response args``) - allows either to register ``Response`` object or directly provide arguments of ``Response`` object. See `Response Parameters`_ .. code-block:: python import responses import requests @responses.activate def test_simple(): # Register via 'Response' object rsp1 = responses.Response( method="PUT", url="http://example.com", ) responses.add(rsp1) # register via direct arguments 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") resp2 = requests.put("http://example.com") assert resp.json() == {"error": "not found"} assert resp.status_code == 404 assert resp2.status_code == 200 assert resp2.request.method == "PUT" 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") Shortcuts ^^^^^^^^^ Shortcuts provide a shorten version of ``responses.add()`` where method argument is prefilled * responses.delete(``Response args``) - register DELETE response * responses.get(``Response args``) - register GET response * responses.head(``Response args``) - register HEAD response * responses.options(``Response args``) - register OPTIONS response * responses.patch(``Response args``) - register PATCH response * responses.post(``Response args``) - register POST response * responses.put(``Response args``) - register PUT response .. code-block:: python import responses import requests @responses.activate def test_simple(): responses.get( "http://twitter.com/api/1/foobar", json={"type": "get"}, ) responses.post( "http://twitter.com/api/1/foobar", json={"type": "post"}, ) responses.patch( "http://twitter.com/api/1/foobar", json={"type": "patch"}, ) resp_get = requests.get("http://twitter.com/api/1/foobar") resp_post = requests.post("http://twitter.com/api/1/foobar") resp_patch = requests.patch("http://twitter.com/api/1/foobar") assert resp_get.json() == {"type": "get"} assert resp_post.json() == {"type": "post"} assert resp_patch.json() == {"type": "patch"} Responses as a context manager ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Instead of wrapping the whole function with decorator you can use 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 Response Parameters ------------------- 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`` or ``Exception``) The response body. Read more `Exception as 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 (``tuple``) An iterable (``tuple`` is recommended) 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`_ Exception as Response body -------------------------- 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.get("http://twitter.com/api/1/foobar", body=Exception("...")) with pytest.raises(Exception): requests.get("http://twitter.com/api/1/foobar") 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.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.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.get( url=url, body="test", match=[matchers.query_param_matcher(params)], ) 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 By default, matcher will validate that all parameters match strictly. To validate that only parameters specified in the matcher are present in original request use ``strict_match=False``. 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.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. Only following arguments are supported: ``timeout``, ``verify``, ``proxies``, ``stream``, ``cert``. 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.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.get( url, 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.get( url="http://example.com/", body="hello world", match=[matchers.header_matcher({"Accept": "text/plain"})], ) 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.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 --------------------------- Default 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. Ordered Registry ^^^^^^^^^^^^^^^^ In some scenarios it is important to preserve the order of the requests and responses. You can use ``registries.OrderedRegistry`` to force all ``Response`` objects to be dependent on the insertion order and invocation index. In following example we add multiple ``Response`` objects that target the same URL. However, you can see, that status code will depend on the invocation order. .. code-block:: python import requests import responses from responses.registries import OrderedRegistry @responses.activate(registry=OrderedRegistry) def test_invocation_index(): responses.get( "http://twitter.com/api/1/foobar", json={"msg": "not found"}, status=404, ) responses.get( "http://twitter.com/api/1/foobar", json={"msg": "OK"}, status=200, ) responses.get( "http://twitter.com/api/1/foobar", json={"msg": "OK"}, status=200, ) responses.get( "http://twitter.com/api/1/foobar", json={"msg": "not found"}, status=404, ) resp = requests.get("http://twitter.com/api/1/foobar") assert resp.status_code == 404 resp = requests.get("http://twitter.com/api/1/foobar") assert resp.status_code == 200 resp = requests.get("http://twitter.com/api/1/foobar") assert resp.status_code == 200 resp = requests.get("http://twitter.com/api/1/foobar") assert resp.status_code == 404 Custom Registry ^^^^^^^^^^^^^^^ Built-in ``registries`` are 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 print("Before tests:", responses.mock.get_registry()) """ Before tests: """ # using function decorator @responses.activate(registry=CustomRegistry) def run(): print("Within test:", responses.mock.get_registry()) """ Within test: <__main__.CustomRegistry object> """ run() print("After test:", responses.mock.get_registry()) """ After test: """ # using context manager with responses.RequestsMock(registry=CustomRegistry) as rsps: print("In context manager:", rsps.get_registry()) """ In context manager: <__main__.CustomRegistry object> """ print("After exit from context manager:", responses.mock.get_registry()) """ 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", ) Integration with unit test frameworks ------------------------------------- Responses as a ``pytest`` fixture ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Use the pytest-responses package to export ``responses`` as a pytest fixture. ``pip install pytest-responses`` You can then access it in a pytest script using: .. code-block:: python import pytest_responses def test_api(responses): 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 Add default responses for each test ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ When run with ``unittest`` tests, this can be used to set up some generic class-level responses, that may be complemented by each test. Similar interface could be applied in ``pytest`` framework. .. code-block:: python class TestMyApi(unittest.TestCase): def setUp(self): responses.get("https://example.com", body="within setup") # here go other self.responses.add(...) @responses.activate def test_my_func(self): 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 RequestMock methods: start, stop, reset ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ``responses`` has ``start``, ``stop``, ``reset`` methods very analogous to `unittest.mock.patch `_. These make it simpler to do requests mocking in ``setup`` methods or where you want to do multiple patches without nesting decorators or with statements. .. code-block:: python class TestUnitTestPatchSetup: def setup(self): """Creates ``RequestsMock`` instance and starts it.""" self.r_mock = responses.RequestsMock(assert_all_requests_are_fired=True) self.r_mock.start() # optionally some default responses could be registered self.r_mock.get("https://example.com", status=505) self.r_mock.put("https://example.com", status=506) def teardown(self): """Stops and resets RequestsMock instance. If ``assert_all_requests_are_fired`` is set to ``True``, will raise an error if some requests were not processed. """ self.r_mock.stop() self.r_mock.reset() def test_function(self): resp = requests.get("https://example.com") assert resp.status_code == 505 resp = requests.put("https://example.com") assert resp.status_code == 506 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 Request Call Count ------------------------- Assert based on ``Response`` object ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Each ``Response`` object has ``call_count`` attribute that could be inspected to check how many times each request was matched. .. code-block:: python @responses.activate def test_call_count_with_matcher(): rsp = responses.get( "http://www.example.com", match=(matchers.query_param_matcher({}),), ) rsp2 = responses.get( "http://www.example.com", match=(matchers.query_param_matcher({"hello": "world"}),), status=777, ) requests.get("http://www.example.com") resp1 = requests.get("http://www.example.com") requests.get("http://www.example.com?hello=world") resp2 = requests.get("http://www.example.com?hello=world") assert resp1.status_code == 200 assert resp2.status_code == 777 assert rsp.call_count == 2 assert rsp2.call_count == 2 Assert based on the exact URL ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Assert that the request was called exactly n times. .. code-block:: python import responses import requests @responses.activate def test_assert_call_count(): 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) ) @responses.activate def test_assert_call_count_always_match_qs(): responses.get("http://www.example.com") requests.get("http://www.example.com") requests.get("http://www.example.com?hello=world") # One call on each url, querystring is matched by default responses.assert_call_count("http://www.example.com", 1) is True responses.assert_call_count("http://www.example.com?hello=world", 1) is True Assert Request Calls data ------------------------- ``Request`` object has ``calls`` list which elements correspond to ``Call`` objects in the global list of ``Registry``. This can be useful when the order of requests is not guaranteed, but you need to check their correctness, for example in multithreaded applications. .. code-block:: python import concurrent.futures import responses import requests @responses.activate def test_assert_calls_on_resp(): rsp1 = responses.patch("http://www.foo.bar/1/", status=200) rsp2 = responses.patch("http://www.foo.bar/2/", status=400) rsp3 = responses.patch("http://www.foo.bar/3/", status=200) def update_user(uid, is_active): url = f"http://www.foo.bar/{uid}/" response = requests.patch(url, json={"is_active": is_active}) return response with concurrent.futures.ThreadPoolExecutor(max_workers=3) as executor: future_to_uid = { executor.submit(update_user, uid, is_active): uid for (uid, is_active) in [("3", True), ("2", True), ("1", False)] } for future in concurrent.futures.as_completed(future_to_uid): uid = future_to_uid[future] response = future.result() print(f"{uid} updated with {response.status_code} status code") assert len(responses.calls) == 3 # total calls count assert rsp1.call_count == 1 assert rsp1.calls[0] in responses.calls assert rsp1.calls[0].response.status_code == 200 assert json.loads(rsp1.calls[0].request.body) == {"is_active": False} assert rsp2.call_count == 1 assert rsp2.calls[0] in responses.calls assert rsp2.calls[0].response.status_code == 400 assert json.loads(rsp2.calls[0].request.body) == {"is_active": True} assert rsp3.call_count == 1 assert rsp3.calls[0] in responses.calls assert rsp3.calls[0].response.status_code == 200 assert json.loads(rsp3.calls[0].request.body) == {"is_active": True} 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.get("http://twitter.com/api/1/foobar", status=500) 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 URL Redirection --------------- In the following example you can see how to create a redirection chain and add custom exception that will be raised in the execution chain and contain the history of redirects. .. code-block:: A -> 301 redirect -> B B -> 301 redirect -> C C -> connection issue .. code-block:: python import pytest import requests import responses @responses.activate def test_redirect(): # create multiple Response objects where first two contain redirect headers rsp1 = responses.Response( responses.GET, "http://example.com/1", status=301, headers={"Location": "http://example.com/2"}, ) rsp2 = responses.Response( responses.GET, "http://example.com/2", status=301, headers={"Location": "http://example.com/3"}, ) rsp3 = responses.Response(responses.GET, "http://example.com/3", status=200) # register above generated Responses in ``response`` module responses.add(rsp1) responses.add(rsp2) responses.add(rsp3) # do the first request in order to generate genuine ``requests`` response # this object will contain genuine attributes of the response, like ``history`` rsp = requests.get("http://example.com/1") responses.calls.reset() # customize exception with ``response`` attribute my_error = requests.ConnectionError("custom error") my_error.response = rsp # update body of the 3rd response with Exception, this will be raised during execution rsp3.body = my_error with pytest.raises(requests.ConnectionError) as exc_info: requests.get("http://example.com/1") assert exc_info.value.args[0] == "custom error" assert rsp1.url in exc_info.value.response.history[0].url assert rsp2.url in exc_info.value.response.history[1].url Validate ``Retry`` mechanism ---------------------------- If you are using the ``Retry`` features of ``urllib3`` and want to cover scenarios that test your retry limits, you can test those scenarios with ``responses`` as well. The best approach will be to use an `Ordered Registry`_ .. code-block:: python import requests import responses from responses import registries from urllib3.util import Retry @responses.activate(registry=registries.OrderedRegistry) def test_max_retries(): url = "https://example.com" rsp1 = responses.get(url, body="Error", status=500) rsp2 = responses.get(url, body="Error", status=500) rsp3 = responses.get(url, body="Error", status=500) rsp4 = responses.get(url, body="OK", status=200) session = requests.Session() adapter = requests.adapters.HTTPAdapter( max_retries=Retry( total=4, backoff_factor=0.1, status_forcelist=[500], method_whitelist=["GET", "POST", "PATCH"], ) ) session.mount("https://", adapter) resp = session.get(url) assert resp.status_code == 200 assert rsp1.call_count == 1 assert rsp2.call_count == 1 assert rsp3.call_count == 1 assert rsp4.call_count == 1 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 ``passthrough`` argument of the ``Response`` object to force 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", 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.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. Coroutines and Multithreading ----------------------------- ``responses`` supports both Coroutines and Multithreading out of the box. Note, ``responses`` locks threading on ``RequestMock`` object allowing only single thread to access it. .. code-block:: python async def test_async_calls(): @responses.activate async def run(): 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 responses.calls[0].request.url == "http://twitter.com/api/1/foobar" await run() BETA Features ------------- Below you can find a list of BETA features. Although we will try to keep the API backwards compatible with released version, we reserve the right to change these APIs before they are considered stable. Please share your feedback via `GitHub Issues `_. Record Responses to files ^^^^^^^^^^^^^^^^^^^^^^^^^ You can perform real requests to the server and ``responses`` will automatically record the output to the file. Recorded data is stored in `YAML `_ format. Apply ``@responses._recorder.record(file_path="out.yaml")`` decorator to any function where you perform requests to record responses to ``out.yaml`` file. Following code .. code-block:: python import requests from responses import _recorder def another(): rsp = requests.get("https://httpstat.us/500") rsp = requests.get("https://httpstat.us/202") @_recorder.record(file_path="out.yaml") def test_recorder(): rsp = requests.get("https://httpstat.us/404") rsp = requests.get("https://httpbin.org/status/wrong") another() will produce next output: .. code-block:: yaml responses: - response: auto_calculate_content_length: false body: 404 Not Found content_type: text/plain method: GET status: 404 url: https://httpstat.us/404 - response: auto_calculate_content_length: false body: Invalid status code content_type: text/plain method: GET status: 400 url: https://httpbin.org/status/wrong - response: auto_calculate_content_length: false body: 500 Internal Server Error content_type: text/plain method: GET status: 500 url: https://httpstat.us/500 - response: auto_calculate_content_length: false body: 202 Accepted content_type: text/plain method: GET status: 202 url: https://httpstat.us/202 If you are in the REPL, you can also activete the recorder for all following responses: .. code-block:: python import requests from responses import _recorder _recorder.recorder.start() requests.get("https://httpstat.us/500") _recorder.recorder.dump_to_file("out.yaml") # you can stop or reset the recorder _recorder.recorder.stop() _recorder.recorder.reset() Replay responses (populate registry) from files ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ You can populate your active registry from a ``yaml`` file with recorded responses. (See `Record Responses to files`_ to understand how to obtain a file). To do that you need to execute ``responses._add_from_file(file_path="out.yaml")`` within an activated decorator or a context manager. The following code example registers a ``patch`` response, then all responses present in ``out.yaml`` file and a ``post`` response at the end. .. code-block:: python import responses @responses.activate def run(): responses.patch("http://httpbin.org") responses._add_from_file(file_path="out.yaml") responses.post("http://httpbin.org/form") run() 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 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1736802182.0 responses-0.25.6/responses.egg-info/SOURCES.txt0000644000175100001660000000110214741277606020637 0ustar00runnerdockerCHANGES LICENSE MANIFEST.in README.rst pyproject.toml setup.py tox.ini responses/__init__.py responses/_recorder.py responses/matchers.py responses/py.typed responses/registries.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 responses/tests/__init__.py responses/tests/test_matchers.py responses/tests/test_multithreading.py responses/tests/test_recorder.py responses/tests/test_registries.py responses/tests/test_responses.py././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1736802182.0 responses-0.25.6/responses.egg-info/dependency_links.txt0000644000175100001660000000000114741277606023027 0ustar00runnerdocker ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1736802182.0 responses-0.25.6/responses.egg-info/not-zip-safe0000644000175100001660000000000114741277606021207 0ustar00runnerdocker ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1736802182.0 responses-0.25.6/responses.egg-info/requires.txt0000644000175100001660000000033514741277606021362 0ustar00runnerdockerrequests<3.0,>=2.30.0 urllib3<3.0,>=1.25.10 pyyaml [tests] pytest>=7.0.0 coverage>=6.0.0 pytest-cov pytest-asyncio pytest-httpserver flake8 types-PyYAML types-requests mypy tomli-w [tests:python_version < "3.11"] tomli ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1736802182.0 responses-0.25.6/responses.egg-info/top_level.txt0000644000175100001660000000001214741277606021504 0ustar00runnerdockerresponses ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1736802182.8943353 responses-0.25.6/setup.cfg0000644000175100001660000000004614741277607015070 0ustar00runnerdocker[egg_info] tag_build = tag_date = 0 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1736802180.0 responses-0.25.6/setup.py0000644000175100001660000000405214741277604014757 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 """ from setuptools import setup install_requires = [ "requests>=2.30.0,<3.0", "urllib3>=1.25.10,<3.0", "pyyaml", ] tests_require = [ "pytest>=7.0.0", "coverage >= 6.0.0", "pytest-cov", "pytest-asyncio", "pytest-httpserver", "flake8", "types-PyYAML", "types-requests", "mypy", # for check of different parsers in recorder "tomli; python_version < '3.11'", "tomli-w", ] extras_require = {"tests": tests_require} setup( name="responses", version="0.25.6", author="David Cramer", description="A utility library for mocking out the `requests` Python library.", url="https://github.com/getsentry/responses", project_urls={ "Bug Tracker": "https://github.com/getsentry/responses/issues", "Changes": "https://github.com/getsentry/responses/blob/master/CHANGES", "Documentation": "https://github.com/getsentry/responses/blob/master/README.rst", "Source Code": "https://github.com/getsentry/responses", }, license="Apache 2.0", long_description=open("README.rst", encoding="utf-8").read(), long_description_content_type="text/x-rst", packages=["responses"], package_data={ "responses": ["py.typed"], }, zip_safe=False, python_requires=">=3.8", install_requires=install_requires, extras_require=extras_require, classifiers=[ "Intended Audience :: Developers", "Intended Audience :: System Administrators", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Topic :: Software Development", ], ) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1736802180.0 responses-0.25.6/tox.ini0000644000175100001660000000126514741277604014563 0ustar00runnerdocker[tox] envlist = py38,py39,py310,py311,py312,mypy,precom [pytest] filterwarnings = error default::DeprecationWarning [testenv] extras = tests commands = pytest . --asyncio-mode=auto --cov responses --cov-report term-missing {posargs} [testenv:mypy] description = Check types using 'mypy' basepython = python3.10 commands = python -m mypy --config-file=mypy.ini -p responses # see https://github.com/getsentry/responses/issues/556 python -m mypy --config-file=mypy.ini --namespace-packages -p responses [testenv:precom] description = Run pre-commit hooks (black, flake, etc) basepython = python3.10 deps = pre-commit>=2.9.2 commands = pre-commit run --all-files