From cd1c6d3e6c3c80bf55fbfd6a657ba642c6fb4e3f Mon Sep 17 00:00:00 2001 From: Steve Pulec Date: Thu, 5 Apr 2018 16:57:43 -0400 Subject: [PATCH] Unvendor responses, move back to upstream. --- moto/apigateway/models.py | 2 +- moto/core/models.py | 87 ++++- moto/packages/responses/.gitignore | 12 - moto/packages/responses/.travis.yml | 27 -- moto/packages/responses/CHANGES | 32 -- moto/packages/responses/LICENSE | 201 ---------- moto/packages/responses/MANIFEST.in | 2 - moto/packages/responses/Makefile | 16 - moto/packages/responses/README.rst | 190 --------- moto/packages/responses/__init__.py | 0 moto/packages/responses/responses.py | 330 ---------------- moto/packages/responses/setup.cfg | 5 - moto/packages/responses/setup.py | 99 ----- moto/packages/responses/test_responses.py | 444 ---------------------- moto/packages/responses/tox.ini | 11 - moto/s3/urls.py | 4 +- setup.py | 1 + tests/test_apigateway/test_apigateway.py | 2 +- tests/test_sns/test_publishing_boto3.py | 2 +- 19 files changed, 79 insertions(+), 1388 deletions(-) delete mode 100644 moto/packages/responses/.gitignore delete mode 100644 moto/packages/responses/.travis.yml delete mode 100644 moto/packages/responses/CHANGES delete mode 100644 moto/packages/responses/LICENSE delete mode 100644 moto/packages/responses/MANIFEST.in delete mode 100644 moto/packages/responses/Makefile delete mode 100644 moto/packages/responses/README.rst delete mode 100644 moto/packages/responses/__init__.py delete mode 100644 moto/packages/responses/responses.py delete mode 100644 moto/packages/responses/setup.cfg delete mode 100644 moto/packages/responses/setup.py delete mode 100644 moto/packages/responses/test_responses.py delete mode 100644 moto/packages/responses/tox.ini diff --git a/moto/apigateway/models.py b/moto/apigateway/models.py index 27a9b86c2..a419a3afa 100644 --- a/moto/apigateway/models.py +++ b/moto/apigateway/models.py @@ -6,7 +6,7 @@ import string import requests import time -from moto.packages.responses import responses +import responses from moto.core import BaseBackend, BaseModel from .utils import create_id from .exceptions import StageNotFoundException diff --git a/moto/core/models.py b/moto/core/models.py index c6fb72ffa..c9895c0cb 100644 --- a/moto/core/models.py +++ b/moto/core/models.py @@ -9,7 +9,7 @@ import re import six from moto import settings -from moto.packages.responses import responses +import responses from moto.packages.httpretty import HTTPretty from .utils import ( convert_httpretty_response, @@ -124,31 +124,90 @@ RESPONSES_METHODS = [responses.GET, responses.DELETE, responses.HEAD, responses.OPTIONS, responses.PATCH, responses.POST, responses.PUT] -class ResponsesMockAWS(BaseMockAWS): +class CallbackResponse(responses.CallbackResponse): + ''' + Need to subclass so we can change a couple things + ''' + def get_response(self, request): + ''' + Need to override this so we can pass decode_content=False + ''' + headers = self.get_headers() + result = self.callback(request) + if isinstance(result, Exception): + raise result + + status, r_headers, body = result + body = responses._handle_body(body) + headers.update(r_headers) + + return responses.HTTPResponse( + status=status, + reason=six.moves.http_client.responses.get(status), + body=body, + headers=headers, + preload_content=False, + # Need to not decode_content to mimic requests + decode_content=False, + ) + + def _url_matches(self, url, other, match_querystring=False): + ''' + Need to override this so we can fix querystrings breaking regex matching + ''' + if not match_querystring: + other = other.split('?', 1)[0] + + if responses._is_string(url): + if responses._has_unicode(url): + url = responses._clean_unicode(url) + if not isinstance(other, six.text_type): + other = other.encode('ascii').decode('utf8') + return self._url_matches_strict(url, other) + elif isinstance(url, responses.Pattern) and url.match(other): + return True + else: + return False + + +botocore_mock = responses.RequestsMock(assert_all_requests_are_fired=False, target='botocore.vendored.requests.adapters.HTTPAdapter.send') + + +class ResponsesMockAWS(BaseMockAWS): def reset(self): + botocore_mock.reset() responses.reset() def enable_patching(self): + botocore_mock.start() responses.start() + for method in RESPONSES_METHODS: for backend in self.backends_for_urls.values(): for key, value in backend.urls.items(): - responses.add_callback( - method=method, - url=re.compile(key), - callback=convert_flask_to_responses_response(value), + responses.add( + CallbackResponse( + method=method, + url=re.compile(key), + callback=convert_flask_to_responses_response(value), + stream=True, + match_querystring=False, + ) + ) + botocore_mock.add( + CallbackResponse( + method=method, + url=re.compile(key), + callback=convert_flask_to_responses_response(value), + stream=True, + match_querystring=False, + ) ) - for pattern in responses.mock._urls: - pattern['stream'] = True - def disable_patching(self): - try: - responses.stop() - except AttributeError: - pass - responses.reset() + botocore_mock.stop() + responses.stop() MockAWS = ResponsesMockAWS diff --git a/moto/packages/responses/.gitignore b/moto/packages/responses/.gitignore deleted file mode 100644 index 5d4406b8d..000000000 --- a/moto/packages/responses/.gitignore +++ /dev/null @@ -1,12 +0,0 @@ -.arcconfig -.coverage -.DS_Store -.idea -*.db -*.egg-info -*.pyc -/htmlcov -/dist -/build -/.cache -/.tox diff --git a/moto/packages/responses/.travis.yml b/moto/packages/responses/.travis.yml deleted file mode 100644 index 9ab219db0..000000000 --- a/moto/packages/responses/.travis.yml +++ /dev/null @@ -1,27 +0,0 @@ -language: python -sudo: false -python: - - "2.6" - - "2.7" - - "3.3" - - "3.4" - - "3.5" -cache: - directories: - - .pip_download_cache -env: - matrix: - - REQUESTS=requests==2.0 - - REQUESTS=-U requests - - REQUESTS="-e git+git://github.com/kennethreitz/requests.git#egg=requests" - global: - - PIP_DOWNLOAD_CACHE=".pip_download_cache" -matrix: - allow_failures: - - env: 'REQUESTS="-e git+git://github.com/kennethreitz/requests.git#egg=requests"' -install: - - "pip install ${REQUESTS}" - - make develop -script: - - if [[ $TRAVIS_PYTHON_VERSION != 2.6 ]]; then make lint; fi - - py.test . --cov responses --cov-report term-missing diff --git a/moto/packages/responses/CHANGES b/moto/packages/responses/CHANGES deleted file mode 100644 index 1bfd7ead8..000000000 --- a/moto/packages/responses/CHANGES +++ /dev/null @@ -1,32 +0,0 @@ -Unreleased ----------- - -- Allow empty list/dict as json object (GH-100) - -0.5.1 ------ - -- Add LICENSE, README and CHANGES to the PyPI distribution (GH-97). - -0.5.0 ------ - -- Allow passing a JSON body to `response.add` (GH-82) -- Improve ConnectionError emulation (GH-73) -- Correct assertion in assert_all_requests_are_fired (GH-71) - -0.4.0 ------ - -- Requests 2.0+ is required -- Mocking now happens on the adapter instead of the session - -0.3.0 ------ - -- Add the ability to mock errors (GH-22) -- Add responses.mock context manager (GH-36) -- Support custom adapters (GH-33) -- Add support for regexp error matching (GH-25) -- Add support for dynamic bodies via `responses.add_callback` (GH-24) -- Preserve argspec when using `responses.activate` decorator (GH-18) diff --git a/moto/packages/responses/LICENSE b/moto/packages/responses/LICENSE deleted file mode 100644 index 52b44b20a..000000000 --- a/moto/packages/responses/LICENSE +++ /dev/null @@ -1,201 +0,0 @@ - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - -TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - -1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - -2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - -3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - -4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - -5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - -6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - -7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - -8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - -9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - -END OF TERMS AND CONDITIONS - -APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - -Copyright 2015 David Cramer - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. diff --git a/moto/packages/responses/MANIFEST.in b/moto/packages/responses/MANIFEST.in deleted file mode 100644 index ef901684c..000000000 --- a/moto/packages/responses/MANIFEST.in +++ /dev/null @@ -1,2 +0,0 @@ -include README.rst CHANGES LICENSE -global-exclude *~ diff --git a/moto/packages/responses/Makefile b/moto/packages/responses/Makefile deleted file mode 100644 index 9da42c6d1..000000000 --- a/moto/packages/responses/Makefile +++ /dev/null @@ -1,16 +0,0 @@ -develop: - pip install -e . - make install-test-requirements - -install-test-requirements: - pip install "file://`pwd`#egg=responses[tests]" - -test: develop lint - @echo "Running Python tests" - py.test . - @echo "" - -lint: - @echo "Linting Python files" - PYFLAKES_NODOCTEST=1 flake8 . - @echo "" diff --git a/moto/packages/responses/README.rst b/moto/packages/responses/README.rst deleted file mode 100644 index 5f946fcde..000000000 --- a/moto/packages/responses/README.rst +++ /dev/null @@ -1,190 +0,0 @@ -Responses -========= - -.. image:: https://travis-ci.org/getsentry/responses.svg?branch=master - :target: https://travis-ci.org/getsentry/responses - -A utility library for mocking out the `requests` Python library. - -.. note:: Responses requires Requests >= 2.0 - -Response body as string ------------------------ - -.. code-block:: python - - import responses - import requests - - @responses.activate - def test_my_api(): - responses.add(responses.GET, 'http://twitter.com/api/1/foobar', - body='{"error": "not found"}', status=404, - content_type='application/json') - - resp = requests.get('http://twitter.com/api/1/foobar') - - assert resp.json() == {"error": "not found"} - - assert len(responses.calls) == 1 - assert responses.calls[0].request.url == 'http://twitter.com/api/1/foobar' - assert responses.calls[0].response.text == '{"error": "not found"}' - -You can also specify a JSON object instead of a body string. - -.. code-block:: python - - import responses - import requests - - @responses.activate - def test_my_api(): - responses.add(responses.GET, 'http://twitter.com/api/1/foobar', - json={"error": "not found"}, status=404) - - resp = requests.get('http://twitter.com/api/1/foobar') - - assert resp.json() == {"error": "not found"} - - assert len(responses.calls) == 1 - assert responses.calls[0].request.url == 'http://twitter.com/api/1/foobar' - assert responses.calls[0].response.text == '{"error": "not found"}' - -Request callback ----------------- - -.. 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' - ) - -Instead of passing a string URL into `responses.add` or `responses.add_callback` -you can also supply a compiled regular expression. - -.. code-block:: python - - import re - import responses - import requests - - # Instead of - responses.add(responses.GET, 'http://twitter.com/api/1/foobar', - body='{"error": "not found"}', status=404, - content_type='application/json') - - # You can do the following - url_re = re.compile(r'https?://twitter\.com/api/\d+/foobar') - responses.add(responses.GET, url_re, - body='{"error": "not found"}', status=404, - content_type='application/json') - -A response can also throw an exception as follows. - -.. code-block:: python - - import responses - import requests - from requests.exceptions import HTTPError - - exception = HTTPError('Something went wrong') - responses.add(responses.GET, 'http://twitter.com/api/1/foobar', - body=exception) - # All calls to 'http://twitter.com/api/1/foobar' will throw exception. - - -Responses as a context manager ------------------------------- - -.. code-block:: python - - import responses - import requests - - - def test_my_api(): - with responses.RequestsMock() as rsps: - rsps.add(responses.GET, 'http://twitter.com/api/1/foobar', - body='{}', status=200, - content_type='application/json') - resp = requests.get('http://twitter.com/api/1/foobar') - - assert resp.status_code == 200 - - # outside the context manager requests will hit the remote server - resp = requests.get('http://twitter.com/api/1/foobar') - resp.status_code == 404 - - -Assertions on declared responses --------------------------------- - -When used as a context manager, Responses will, by default, raise an assertion -error if a url was registered but not accessed. This can be disabled by passing -the ``assert_all_requests_are_fired`` value: - -.. code-block:: python - - import responses - import requests - - - def test_my_api(): - with responses.RequestsMock(assert_all_requests_are_fired=False) as rsps: - rsps.add(responses.GET, 'http://twitter.com/api/1/foobar', - body='{}', status=200, - content_type='application/json') - -Multiple Responses ------------------- -You can also use ``assert_all_requests_are_fired`` to add multiple responses for the same url: - -.. code-block:: python - - import responses - import requests - - - def test_my_api(): - with responses.RequestsMock(assert_all_requests_are_fired=True) as rsps: - rsps.add(responses.GET, 'http://twitter.com/api/1/foobar', status=500) - 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 == 500 - resp = requests.get('http://twitter.com/api/1/foobar') - assert resp.status_code == 200 diff --git a/moto/packages/responses/__init__.py b/moto/packages/responses/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/moto/packages/responses/responses.py b/moto/packages/responses/responses.py deleted file mode 100644 index 3bc437f0b..000000000 --- a/moto/packages/responses/responses.py +++ /dev/null @@ -1,330 +0,0 @@ -from __future__ import ( - absolute_import, print_function, division, unicode_literals -) - -import inspect -import json as json_module -import re -import six - -from collections import namedtuple, Sequence, Sized -from functools import update_wrapper -from cookies import Cookies -from requests.adapters import HTTPAdapter -from requests.utils import cookiejar_from_dict -from requests.exceptions import ConnectionError -from requests.sessions import REDIRECT_STATI - -try: - from requests.packages.urllib3.response import HTTPResponse -except ImportError: - from urllib3.response import HTTPResponse - -if six.PY2: - from urlparse import urlparse, parse_qsl -else: - from urllib.parse import urlparse, parse_qsl - -if six.PY2: - try: - from six import cStringIO as BufferIO - except ImportError: - from six import StringIO as BufferIO -else: - from io import BytesIO as BufferIO - - -Call = namedtuple('Call', ['request', 'response']) - -_wrapper_template = """\ -def wrapper%(signature)s: - with responses: - return func%(funcargs)s -""" - - -def _is_string(s): - return isinstance(s, (six.string_types, six.text_type)) - - -def _is_redirect(response): - try: - # 2.0.0 <= requests <= 2.2 - return response.is_redirect - except AttributeError: - # requests > 2.2 - return ( - # use request.sessions conditional - response.status_code in REDIRECT_STATI and - 'location' in response.headers - ) - - -def get_wrapped(func, wrapper_template, evaldict): - # Preserve the argspec for the wrapped function so that testing - # tools such as pytest can continue to use their fixture injection. - args, a, kw, defaults = inspect.getargspec(func) - - signature = inspect.formatargspec(args, a, kw, defaults) - is_bound_method = hasattr(func, '__self__') - if is_bound_method: - args = args[1:] # Omit 'self' - callargs = inspect.formatargspec(args, a, kw, None) - - ctx = {'signature': signature, 'funcargs': callargs} - six.exec_(wrapper_template % ctx, evaldict) - - wrapper = evaldict['wrapper'] - - update_wrapper(wrapper, func) - if is_bound_method: - wrapper = wrapper.__get__(func.__self__, type(func.__self__)) - return wrapper - - -class CallList(Sequence, Sized): - - def __init__(self): - self._calls = [] - - def __iter__(self): - return iter(self._calls) - - def __len__(self): - return len(self._calls) - - def __getitem__(self, idx): - return self._calls[idx] - - def add(self, request, response): - self._calls.append(Call(request, response)) - - def reset(self): - self._calls = [] - - -def _ensure_url_default_path(url, match_querystring): - if _is_string(url) and url.count('/') == 2: - if match_querystring: - return url.replace('?', '/?', 1) - else: - return url + '/' - return url - - -class RequestsMock(object): - DELETE = 'DELETE' - GET = 'GET' - HEAD = 'HEAD' - OPTIONS = 'OPTIONS' - PATCH = 'PATCH' - POST = 'POST' - PUT = 'PUT' - - def __init__(self, assert_all_requests_are_fired=True, pass_through=True): - self._calls = CallList() - self.reset() - self.assert_all_requests_are_fired = assert_all_requests_are_fired - self.pass_through = pass_through - self.original_send = HTTPAdapter.send - - def reset(self): - self._urls = [] - self._calls.reset() - - def add(self, method, url, body='', match_querystring=False, - status=200, adding_headers=None, stream=False, - content_type='text/plain', json=None): - - # if we were passed a `json` argument, - # override the body and content_type - if json is not None: - body = json_module.dumps(json) - content_type = 'application/json' - - # ensure the url has a default path set if the url is a string - url = _ensure_url_default_path(url, match_querystring) - - # body must be bytes - if isinstance(body, six.text_type): - body = body.encode('utf-8') - - self._urls.append({ - 'url': url, - 'method': method, - 'body': body, - 'content_type': content_type, - 'match_querystring': match_querystring, - 'status': status, - 'adding_headers': adding_headers, - 'stream': stream, - }) - - def add_callback(self, method, url, callback, match_querystring=False, - content_type='text/plain'): - # ensure the url has a default path set if the url is a string - # url = _ensure_url_default_path(url, match_querystring) - - self._urls.append({ - 'url': url, - 'method': method, - 'callback': callback, - 'content_type': content_type, - 'match_querystring': match_querystring, - }) - - @property - def calls(self): - return self._calls - - def __enter__(self): - self.start() - return self - - def __exit__(self, type, value, traceback): - success = type is None - self.stop(allow_assert=success) - self.reset() - return success - - def activate(self, func): - evaldict = {'responses': self, 'func': func} - return get_wrapped(func, _wrapper_template, evaldict) - - def _find_match(self, request): - for match in self._urls: - if request.method != match['method']: - continue - - if not self._has_url_match(match, request.url): - continue - - break - else: - return None - if self.assert_all_requests_are_fired: - # for each found match remove the url from the stack - self._urls.remove(match) - return match - - def _has_url_match(self, match, request_url): - url = match['url'] - - if not match['match_querystring']: - request_url = request_url.split('?', 1)[0] - - if _is_string(url): - if match['match_querystring']: - return self._has_strict_url_match(url, request_url) - else: - return url == request_url - elif isinstance(url, re._pattern_type) and url.match(request_url): - return True - else: - return False - - def _has_strict_url_match(self, url, other): - url_parsed = urlparse(url) - other_parsed = urlparse(other) - - if url_parsed[:3] != other_parsed[:3]: - return False - - url_qsl = sorted(parse_qsl(url_parsed.query)) - other_qsl = sorted(parse_qsl(other_parsed.query)) - return url_qsl == other_qsl - - def _on_request(self, adapter, request, **kwargs): - match = self._find_match(request) - # TODO(dcramer): find the correct class for this - if match is None: - if self.pass_through: - return self.original_send(adapter, request, **kwargs) - - error_msg = 'Connection refused: {0} {1}'.format(request.method, - request.url) - response = ConnectionError(error_msg) - response.request = request - - self._calls.add(request, response) - raise response - - if 'body' in match and isinstance(match['body'], Exception): - self._calls.add(request, match['body']) - raise match['body'] - - headers = {} - if match['content_type'] is not None: - headers['Content-Type'] = match['content_type'] - - if 'callback' in match: # use callback - status, r_headers, body = match['callback'](request) - if isinstance(body, six.text_type): - body = body.encode('utf-8') - body = BufferIO(body) - headers.update(r_headers) - - elif 'body' in match: - if match['adding_headers']: - headers.update(match['adding_headers']) - status = match['status'] - body = BufferIO(match['body']) - - response = HTTPResponse( - status=status, - reason=six.moves.http_client.responses[status], - body=body, - headers=headers, - preload_content=False, - # Need to not decode_content to mimic requests - decode_content=False, - ) - - response = adapter.build_response(request, response) - if not match.get('stream'): - response.content # NOQA - - try: - resp_cookies = Cookies.from_request(response.headers['set-cookie']) - response.cookies = cookiejar_from_dict(dict( - (v.name, v.value) - for _, v - in resp_cookies.items() - )) - except (KeyError, TypeError): - pass - - self._calls.add(request, response) - - return response - - def start(self): - try: - from unittest import mock - except ImportError: - import mock - - def unbound_on_send(adapter, request, *a, **kwargs): - return self._on_request(adapter, request, *a, **kwargs) - self._patcher1 = mock.patch('botocore.vendored.requests.adapters.HTTPAdapter.send', - unbound_on_send) - self._patcher1.start() - self._patcher2 = mock.patch('requests.adapters.HTTPAdapter.send', - unbound_on_send) - self._patcher2.start() - - def stop(self, allow_assert=True): - self._patcher1.stop() - self._patcher2.stop() - if allow_assert and self.assert_all_requests_are_fired and self._urls: - raise AssertionError( - 'Not all requests have been executed {0!r}'.format( - [(url['method'], url['url']) for url in self._urls])) - - -# expose default mock namespace -mock = _default_mock = RequestsMock(assert_all_requests_are_fired=False, pass_through=False) -__all__ = [] -for __attr in (a for a in dir(_default_mock) if not a.startswith('_')): - __all__.append(__attr) - globals()[__attr] = getattr(_default_mock, __attr) diff --git a/moto/packages/responses/setup.cfg b/moto/packages/responses/setup.cfg deleted file mode 100644 index 9b6594f2e..000000000 --- a/moto/packages/responses/setup.cfg +++ /dev/null @@ -1,5 +0,0 @@ -[pytest] -addopts=--tb=short - -[bdist_wheel] -universal=1 diff --git a/moto/packages/responses/setup.py b/moto/packages/responses/setup.py deleted file mode 100644 index 911c07da4..000000000 --- a/moto/packages/responses/setup.py +++ /dev/null @@ -1,99 +0,0 @@ -#!/usr/bin/env python -""" -responses -========= - -A utility library for mocking out the `requests` Python library. - -:copyright: (c) 2015 David Cramer -:license: Apache 2.0 -""" - -import sys -import logging - -from setuptools import setup -from setuptools.command.test import test as TestCommand -import pkg_resources - - -setup_requires = [] - -if 'test' in sys.argv: - setup_requires.append('pytest') - -install_requires = [ - 'requests>=2.0', - 'cookies', - 'six', -] - -tests_require = [ - 'pytest', - 'coverage >= 3.7.1, < 5.0.0', - 'pytest-cov', - 'flake8', -] - - -extras_require = { - ':python_version in "2.6, 2.7, 3.2"': ['mock'], - 'tests': tests_require, -} - -try: - if 'bdist_wheel' not in sys.argv: - for key, value in extras_require.items(): - if key.startswith(':') and pkg_resources.evaluate_marker(key[1:]): - install_requires.extend(value) -except Exception: - logging.getLogger(__name__).exception( - 'Something went wrong calculating platform specific dependencies, so ' - "you're getting them all!" - ) - for key, value in extras_require.items(): - if key.startswith(':'): - install_requires.extend(value) - - -class PyTest(TestCommand): - - def finalize_options(self): - TestCommand.finalize_options(self) - self.test_args = ['test_responses.py'] - self.test_suite = True - - def run_tests(self): - # import here, cause outside the eggs aren't loaded - import pytest - errno = pytest.main(self.test_args) - sys.exit(errno) - - -setup( - name='responses', - version='0.6.0', - author='David Cramer', - description=( - 'A utility library for mocking out the `requests` Python library.' - ), - url='https://github.com/getsentry/responses', - license='Apache 2.0', - long_description=open('README.rst').read(), - py_modules=['responses', 'test_responses'], - zip_safe=False, - install_requires=install_requires, - extras_require=extras_require, - tests_require=tests_require, - setup_requires=setup_requires, - cmdclass={'test': PyTest}, - include_package_data=True, - classifiers=[ - 'Intended Audience :: Developers', - 'Intended Audience :: System Administrators', - 'Operating System :: OS Independent', - 'Programming Language :: Python :: 2', - 'Programming Language :: Python :: 3', - 'Topic :: Software Development' - ], -) diff --git a/moto/packages/responses/test_responses.py b/moto/packages/responses/test_responses.py deleted file mode 100644 index 967a535cf..000000000 --- a/moto/packages/responses/test_responses.py +++ /dev/null @@ -1,444 +0,0 @@ -from __future__ import ( - absolute_import, print_function, division, unicode_literals -) - -import re -import requests -import responses -import pytest - -from inspect import getargspec -from requests.exceptions import ConnectionError, HTTPError - - -def assert_reset(): - assert len(responses._default_mock._urls) == 0 - assert len(responses.calls) == 0 - - -def assert_response(resp, body=None, content_type='text/plain'): - assert resp.status_code == 200 - assert resp.reason == 'OK' - if content_type is not None: - assert resp.headers['Content-Type'] == content_type - else: - assert 'Content-Type' not in resp.headers - assert resp.text == body - - -def test_response(): - @responses.activate - def run(): - responses.add(responses.GET, 'http://example.com', body=b'test') - resp = requests.get('http://example.com') - assert_response(resp, 'test') - assert len(responses.calls) == 1 - assert responses.calls[0].request.url == 'http://example.com/' - assert responses.calls[0].response.content == b'test' - - resp = requests.get('http://example.com?foo=bar') - assert_response(resp, 'test') - assert len(responses.calls) == 2 - assert responses.calls[1].request.url == 'http://example.com/?foo=bar' - assert responses.calls[1].response.content == b'test' - - run() - assert_reset() - - -def test_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') - - 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_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_throw_connection_error_explicit(): - @responses.activate - def run(): - url = 'http://example.com' - exception = HTTPError('HTTP Error') - responses.add( - responses.GET, url, exception) - - with pytest.raises(HTTPError) as HE: - requests.get(url) - - assert str(HE.value) == 'HTTP Error' - - run() - assert_reset() - - -def test_callback(): - body = b'test callback' - status = 400 - reason = 'Bad Request' - headers = {'foo': 'bar'} - url = 'http://example.com/' - - def request_callback(request): - return (status, headers, body) - - @responses.activate - def run(): - responses.add_callback(responses.GET, url, request_callback) - resp = requests.get(url) - assert resp.text == "test callback" - assert resp.status_code == status - assert resp.reason == reason - assert 'foo' in resp.headers - assert resp.headers['foo'] == 'bar' - - run() - assert_reset() - - -def test_callback_no_content_type(): - body = b'test callback' - status = 400 - reason = 'Bad Request' - headers = {'foo': 'bar'} - url = 'http://example.com/' - - def request_callback(request): - return (status, headers, body) - - @responses.activate - def run(): - responses.add_callback( - responses.GET, url, request_callback, content_type=None) - resp = requests.get(url) - assert resp.text == "test callback" - assert resp.status_code == status - assert resp.reason == reason - assert 'foo' in resp.headers - assert 'Content-Type' not in resp.headers - - run() - assert_reset() - - -def test_regular_expression_url(): - @responses.activate - def run(): - url = re.compile(r'https?://(.*\.)?example.com') - responses.add(responses.GET, url, body=b'test') - - resp = requests.get('http://example.com') - assert_response(resp, 'test') - - resp = requests.get('https://example.com') - assert_response(resp, 'test') - - resp = requests.get('https://uk.example.com') - assert_response(resp, 'test') - - with pytest.raises(ConnectionError): - requests.get('https://uk.exaaample.com') - - run() - assert_reset() - - -def test_custom_adapter(): - @responses.activate - def run(): - url = "http://example.com" - responses.add(responses.GET, url, body=b'test') - - calls = [0] - - class DummyAdapter(requests.adapters.HTTPAdapter): - - def send(self, *a, **k): - calls[0] += 1 - return super(DummyAdapter, self).send(*a, **k) - - # Test that the adapter is actually used - session = requests.Session() - session.mount("http://", DummyAdapter()) - - resp = session.get(url, allow_redirects=False) - assert calls[0] == 1 - - # Test that the response is still correctly emulated - session = requests.Session() - session.mount("http://", DummyAdapter()) - - resp = session.get(url) - assert_response(resp, 'test') - - run() - - -def test_responses_as_context_manager(): - def run(): - with responses.mock: - responses.add(responses.GET, 'http://example.com', body=b'test') - resp = requests.get('http://example.com') - assert_response(resp, 'test') - assert len(responses.calls) == 1 - assert responses.calls[0].request.url == 'http://example.com/' - assert responses.calls[0].response.content == b'test' - - resp = requests.get('http://example.com?foo=bar') - assert_response(resp, 'test') - assert len(responses.calls) == 2 - assert (responses.calls[1].request.url == - 'http://example.com/?foo=bar') - assert responses.calls[1].response.content == b'test' - - run() - assert_reset() - - -def test_activate_doesnt_change_signature(): - def test_function(a, b=None): - return (a, b) - - decorated_test_function = responses.activate(test_function) - assert getargspec(test_function) == getargspec(decorated_test_function) - assert decorated_test_function(1, 2) == test_function(1, 2) - assert decorated_test_function(3) == test_function(3) - - -def test_activate_doesnt_change_signature_for_method(): - class TestCase(object): - - def test_function(self, a, b=None): - return (self, a, b) - - test_case = TestCase() - argspec = getargspec(test_case.test_function) - decorated_test_function = responses.activate(test_case.test_function) - assert argspec == getargspec(decorated_test_function) - assert decorated_test_function(1, 2) == test_case.test_function(1, 2) - assert decorated_test_function(3) == test_case.test_function(3) - - -def test_response_cookies(): - body = b'test callback' - status = 200 - headers = {'set-cookie': 'session_id=12345; a=b; c=d'} - url = 'http://example.com/' - - def request_callback(request): - return (status, headers, body) - - @responses.activate - def run(): - responses.add_callback(responses.GET, url, request_callback) - resp = requests.get(url) - assert resp.text == "test callback" - assert resp.status_code == status - assert 'session_id' in resp.cookies - assert resp.cookies['session_id'] == '12345' - assert resp.cookies['a'] == 'b' - assert resp.cookies['c'] == 'd' - run() - assert_reset() - - -def test_assert_all_requests_are_fired(): - def run(): - with pytest.raises(AssertionError) as excinfo: - with responses.RequestsMock( - assert_all_requests_are_fired=True) as m: - m.add(responses.GET, 'http://example.com', body=b'test') - assert 'http://example.com' in str(excinfo.value) - assert responses.GET in str(excinfo) - - # check that assert_all_requests_are_fired default to True - with pytest.raises(AssertionError): - with responses.RequestsMock() as m: - m.add(responses.GET, 'http://example.com', body=b'test') - - # check that assert_all_requests_are_fired doesn't swallow exceptions - with pytest.raises(ValueError): - with responses.RequestsMock() as m: - m.add(responses.GET, 'http://example.com', body=b'test') - raise ValueError() - - run() - assert_reset() - - -def test_allow_redirects_samehost(): - redirecting_url = 'http://example.com' - final_url_path = '/1' - final_url = '{0}{1}'.format(redirecting_url, final_url_path) - url_re = re.compile(r'^http://example.com(/)?(\d+)?$') - - def request_callback(request): - # endpoint of chained redirect - if request.url.endswith(final_url_path): - return 200, (), b'test' - # otherwise redirect to an integer path - else: - if request.url.endswith('/0'): - n = 1 - else: - n = 0 - redirect_headers = {'location': '/{0!s}'.format(n)} - return 301, redirect_headers, None - - def run(): - # setup redirect - with responses.mock: - responses.add_callback(responses.GET, url_re, request_callback) - resp_no_redirects = requests.get(redirecting_url, - allow_redirects=False) - assert resp_no_redirects.status_code == 301 - assert len(responses.calls) == 1 # 1x300 - assert responses.calls[0][1].status_code == 301 - assert_reset() - - with responses.mock: - responses.add_callback(responses.GET, url_re, request_callback) - resp_yes_redirects = requests.get(redirecting_url, - allow_redirects=True) - assert len(responses.calls) == 3 # 2x300 + 1x200 - assert len(resp_yes_redirects.history) == 2 - assert resp_yes_redirects.status_code == 200 - assert final_url == resp_yes_redirects.url - status_codes = [call[1].status_code for call in responses.calls] - assert status_codes == [301, 301, 200] - assert_reset() - - run() - assert_reset() diff --git a/moto/packages/responses/tox.ini b/moto/packages/responses/tox.ini deleted file mode 100644 index 0a31c03ab..000000000 --- a/moto/packages/responses/tox.ini +++ /dev/null @@ -1,11 +0,0 @@ - -[tox] -envlist = {py26,py27,py32,py33,py34,py35} - -[testenv] -deps = - pytest - pytest-cov - pytest-flakes -commands = - py.test . --cov responses --cov-report term-missing --flakes diff --git a/moto/s3/urls.py b/moto/s3/urls.py index 1d439a549..af0a9954e 100644 --- a/moto/s3/urls.py +++ b/moto/s3/urls.py @@ -21,7 +21,7 @@ url_paths = { '{0}/$': S3ResponseInstance.bucket_response, # subdomain key of path-based bucket - '{0}/(?P[^/]+)/?$': S3ResponseInstance.ambiguous_response, + '{0}/(?P[^/?]+)/?$': S3ResponseInstance.ambiguous_response, # path-based bucket + key - '{0}/(?P[^/]+)/(?P.+)': S3ResponseInstance.key_response, + '{0}/(?P[^/?]+)/(?P.+)': S3ResponseInstance.key_response, } diff --git a/setup.py b/setup.py index 1f135ae7b..d253d7f0a 100755 --- a/setup.py +++ b/setup.py @@ -23,6 +23,7 @@ install_requires = [ "docker>=2.5.1", "jsondiff==1.1.1", "aws-xray-sdk<0.96,>=0.93", + "responses", ] extras_require = { diff --git a/tests/test_apigateway/test_apigateway.py b/tests/test_apigateway/test_apigateway.py index 9e2307bdd..1dc9c976e 100644 --- a/tests/test_apigateway/test_apigateway.py +++ b/tests/test_apigateway/test_apigateway.py @@ -7,7 +7,7 @@ import requests import sure # noqa from botocore.exceptions import ClientError -from moto.packages.responses import responses +import responses from moto import mock_apigateway, settings diff --git a/tests/test_sns/test_publishing_boto3.py b/tests/test_sns/test_publishing_boto3.py index 3ccc3ef44..52347cc15 100644 --- a/tests/test_sns/test_publishing_boto3.py +++ b/tests/test_sns/test_publishing_boto3.py @@ -9,7 +9,7 @@ import re from freezegun import freeze_time import sure # noqa -from moto.packages.responses import responses +import responses from botocore.exceptions import ClientError from moto import mock_sns, mock_sqs from freezegun import freeze_time