From b2026313e304e92312b0cbae3ff266626b068953 Mon Sep 17 00:00:00 2001 From: Jamie Lennox Date: Thu, 25 Aug 2016 12:49:39 +1000 Subject: [PATCH] Allow doing real_http per mock via the mocker If you set up requests_mock to catch all requests (which I would recommend) you sometimes get caught with things like file:// paths that should be allowed to reach the filesystem. To do this we should allow you to add a matcher that says a specific route can bypass the catch all and do a real request. This is a bit of a layer violation but I thought it was easy to start with, then realized why it wasn't. Change-Id: Ic2516f78413b88a489c8d6bd2bc39b8ebb5bf273 Closes-Bug: #1501665 --- docs/mocker.rst | 15 ++++++++++++ requests_mock/adapter.py | 29 ++++++++++++++++++++--- requests_mock/mocker.py | 11 ++++++++- requests_mock/tests/test_matcher.py | 6 +++-- requests_mock/tests/test_mocker.py | 36 +++++++++++++++++++++++++++++ 5 files changed, 91 insertions(+), 6 deletions(-) diff --git a/docs/mocker.rst b/docs/mocker.rst index 60c1d96..f7a2a85 100644 --- a/docs/mocker.rst +++ b/docs/mocker.rst @@ -132,3 +132,18 @@ The Mocker object takes the following parameters: ... 'resp' 200 + +*New in 1.1* + +Similarly when using a mocker you can register an individual URI to bypass the mocking infrastructure and make a real request. Note this only works when using the mocker and not when directly mounting an adapter. + +.. doctest:: + + >>> with requests_mock.Mocker() as m: + ... m.register_uri('GET', 'http://test.com', text='resp') + ... m.register_uri('GET', 'http://www.google.com', real_http=True) + ... print(requests.get('http://test.com').text) + ... print(requests.get('http://www.google.com').status_code) # doctest: +SKIP + ... + 'resp' + 200 diff --git a/requests_mock/adapter.py b/requests_mock/adapter.py index ba587a6..d732a0a 100644 --- a/requests_mock/adapter.py +++ b/requests_mock/adapter.py @@ -151,10 +151,21 @@ class _RequestHistoryTracker(object): return len(self.request_history) +class _RunRealHTTP(Exception): + """A fake exception to jump out of mocking and allow a real request. + + This exception is caught at the mocker level and allows it to execute this + request through the real requests mechanism rather than the mocker. + + It should never be exposed to a user. + """ + + class _Matcher(_RequestHistoryTracker): """Contains all the information about a provided URL to match.""" - def __init__(self, method, url, responses, complete_qs, request_headers): + def __init__(self, method, url, responses, complete_qs, request_headers, + real_http): """ :param bool complete_qs: Match the entire query string. By default URLs match if all the provided matcher query arguments are matched and @@ -162,6 +173,7 @@ class _Matcher(_RequestHistoryTracker): require that the entire query string needs to match. """ super(_Matcher, self).__init__() + self._method = method self._url = url try: @@ -171,6 +183,7 @@ class _Matcher(_RequestHistoryTracker): self._responses = responses self._complete_qs = complete_qs self._request_headers = request_headers + self._real_http = real_http def _match_method(self, request): if self._method is ANY: @@ -248,6 +261,11 @@ class _Matcher(_RequestHistoryTracker): if not self._match(request): return None + # doing this before _add_to_history means real requests are not stored + # in the request history. I'm not sure what is better here. + if self._real_http: + raise _RunRealHTTP() + if len(self._responses) > 1: response_matcher = self._responses.pop(0) else: @@ -294,19 +312,24 @@ class Adapter(BaseAdapter, _RequestHistoryTracker): """ complete_qs = kwargs.pop('complete_qs', False) request_headers = kwargs.pop('request_headers', {}) + real_http = kwargs.pop('_real_http', False) if response_list and kwargs: raise RuntimeError('You should specify either a list of ' 'responses OR response kwargs. Not both.') + elif real_http and (response_list or kwargs): + raise RuntimeError('You should specify either response data ' + 'OR real_http. Not both.') elif not response_list: - response_list = [kwargs] + response_list = [] if real_http else [kwargs] responses = [response._MatcherResponse(**k) for k in response_list] matcher = _Matcher(method, url, responses, complete_qs=complete_qs, - request_headers=request_headers) + request_headers=request_headers, + real_http=real_http) self.add_matcher(matcher) return matcher diff --git a/requests_mock/mocker.py b/requests_mock/mocker.py index c23db25..c7df6af 100644 --- a/requests_mock/mocker.py +++ b/requests_mock/mocker.py @@ -34,7 +34,6 @@ class MockerCore(object): """ _PROXY_FUNCS = set(['last_request', - 'register_uri', 'add_matcher', 'request_history', 'called', @@ -70,6 +69,10 @@ class MockerCore(object): except exceptions.NoMockAddress: if not self._real_http: raise + except adapter._RunRealHTTP: + # this mocker wants you to run the request through the real + # requests library rather than the mocking. Let it. + pass finally: requests.Session.get_adapter = real_get_adapter @@ -95,6 +98,12 @@ class MockerCore(object): raise AttributeError(name) + def register_uri(self, *args, **kwargs): + # you can pass real_http here, but it's private to pass direct to the + # adapter, because if you pass direct to the adapter you'll see the exc + kwargs['_real_http'] = kwargs.pop('real_http', False) + return self._adapter.register_uri(*args, **kwargs) + def request(self, *args, **kwargs): return self.register_uri(*args, **kwargs) diff --git a/requests_mock/tests/test_matcher.py b/requests_mock/tests/test_matcher.py index 03227e3..b0e9b1c 100644 --- a/requests_mock/tests/test_matcher.py +++ b/requests_mock/tests/test_matcher.py @@ -27,12 +27,14 @@ class TestMatcher(base.TestCase): request_method='GET', complete_qs=False, headers=None, - request_headers={}): + request_headers={}, + real_http=False): matcher = adapter._Matcher(matcher_method, target, [], complete_qs, - request_headers) + request_headers, + real_http=real_http) request = adapter._RequestObjectProxy._create(request_method, url, headers) diff --git a/requests_mock/tests/test_mocker.py b/requests_mock/tests/test_mocker.py index 2011c49..7e32813 100644 --- a/requests_mock/tests/test_mocker.py +++ b/requests_mock/tests/test_mocker.py @@ -15,6 +15,7 @@ import requests import requests_mock from requests_mock import compat +from requests_mock import exceptions from requests_mock.tests import base original_send = requests.Session.send @@ -243,3 +244,38 @@ class MockerHttpMethodsTests(base.TestCase): mock_obj = m.delete(self.URL, text=self.TEXT) self.assertResponse(requests.delete(self.URL)) self.assertTrue(mock_obj.called) + + @requests_mock.Mocker() + def test_mocker_real_http_and_responses(self, m): + self.assertRaises(RuntimeError, + m.get, + self.URL, + text='abcd', + real_http=True) + + @requests_mock.Mocker() + def test_mocker_real_http(self, m): + data = 'testdata' + + uri1 = 'fake://example.com/foo' + uri2 = 'fake://example.com/bar' + uri3 = 'fake://example.com/baz' + + m.get(uri1, text=data) + m.get(uri2, real_http=True) + + self.assertEqual(data, requests.get(uri1).text) + + # This should fail because requests can't get an adapter for mock:// + # but it shows that it has tried and would have made a request. + self.assertRaises(requests.exceptions.InvalidSchema, + requests.get, + uri2) + + # This fails because real_http is not set on the mocker + self.assertRaises(exceptions.NoMockAddress, + requests.get, + uri3) + + # do it again to make sure the mock is still in place + self.assertEqual(data, requests.get(uri1).text)