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
This commit is contained in:
Jamie Lennox 2016-08-25 12:49:39 +10:00
parent b3de408600
commit b2026313e3
5 changed files with 91 additions and 6 deletions

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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)

View File

@ -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)