Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/requests_mock/mocker.py: 66%
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1# Licensed under the Apache License, Version 2.0 (the "License"); you may
2# not use this file except in compliance with the License. You may obtain
3# a copy of the License at
4#
5# https://www.apache.org/licenses/LICENSE-2.0
6#
7# Unless required by applicable law or agreed to in writing, software
8# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
9# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
10# License for the specific language governing permissions and limitations
11# under the License.
13import contextlib
14import functools
15import sys
16import threading
17import types
19import requests
21from requests_mock import adapter
22from requests_mock import exceptions
24DELETE = 'DELETE'
25GET = 'GET'
26HEAD = 'HEAD'
27OPTIONS = 'OPTIONS'
28PATCH = 'PATCH'
29POST = 'POST'
30PUT = 'PUT'
32_original_send = requests.Session.send
34# NOTE(phodge): we need to use an RLock (reentrant lock) here because
35# requests.Session.send() is reentrant. See further comments where we
36# monkeypatch get_adapter()
37_send_lock = threading.RLock()
40@contextlib.contextmanager
41def threading_rlock(timeout):
42 kwargs = {}
43 if sys.version_info.major >= 3:
44 # python2 doesn't support the timeout argument
45 kwargs['timeout'] = timeout
47 if not _send_lock.acquire(**kwargs):
48 m = "Could not acquire threading lock - possible deadlock scenario"
49 raise Exception(m)
51 try:
52 yield
53 finally:
54 _send_lock.release()
57def _is_bound_method(method):
58 """
59 bound_method 's self is a obj
60 unbound_method 's self is None
61 """
62 if isinstance(method, types.MethodType) and hasattr(method, '__self__'):
63 return True
65 return False
68def _set_method(target, name, method):
69 """ Set a mocked method onto the target.
71 Target may be either an instance of a Session object of the
72 requests.Session class. First we Bind the method if it's an instance.
74 If method is a bound_method, can direct setattr
75 """
76 if not isinstance(target, type) and not _is_bound_method(method):
77 method = types.MethodType(method, target)
79 setattr(target, name, method)
82class MockerCore(object):
83 """A wrapper around common mocking functions.
85 Automate the process of mocking the requests library. This will keep the
86 same general options available and prevent repeating code.
87 """
89 _PROXY_FUNCS = {
90 'last_request',
91 'add_matcher',
92 'request_history',
93 'called',
94 'called_once',
95 'call_count',
96 'reset',
97 }
99 case_sensitive = False
100 """case_sensitive handles a backwards incompatible bug. The URL used to
101 match against our matches and that is saved in request_history is always
102 lowercased. This is incorrect as it reports incorrect history to the user
103 and doesn't allow case sensitive path matching.
105 Unfortunately fixing this change is backwards incompatible in the 1.X
106 series as people may rely on this behaviour. To work around this you can
107 globally set:
109 requests_mock.mock.case_sensitive = True
111 or for pytest set in your configuration:
113 [pytest]
114 requests_mock_case_sensitive = True
116 which will prevent the lowercase being executed and return case sensitive
117 url and query information.
119 This will become the default in a 2.X release. See bug: #1584008.
120 """
122 def __init__(self, session=None, **kwargs):
123 if session and not isinstance(session, requests.Session):
124 raise TypeError("Only a requests.Session object can be mocked")
126 self._mock_target = session or requests.Session
127 self.case_sensitive = kwargs.pop('case_sensitive', self.case_sensitive)
128 self._adapter = (
129 kwargs.pop('adapter', None) or
130 adapter.Adapter(case_sensitive=self.case_sensitive)
131 )
133 self._json_encoder = kwargs.pop('json_encoder', None)
134 self.real_http = kwargs.pop('real_http', False)
135 self._last_send = None
137 if kwargs:
138 raise TypeError('Unexpected Arguments: %s' % ', '.join(kwargs))
140 def start(self):
141 """Start mocking requests.
143 Install the adapter and the wrappers required to intercept requests.
144 """
145 if self._last_send:
146 raise RuntimeError('Mocker has already been started')
148 # backup last `send` for restoration on `self.stop`
149 self._last_send = self._mock_target.send
150 self._last_get_adapter = self._mock_target.get_adapter
152 def _fake_get_adapter(session, url):
153 return self._adapter
155 def _fake_send(session, request, **kwargs):
156 # NOTE(phodge): we need to use a threading lock here in case there
157 # are multiple threads running - one thread could restore the
158 # original get_adapter() just as a second thread is about to
159 # execute _original_send() below
160 with threading_rlock(timeout=10):
161 # mock get_adapter
162 #
163 # NOTE(phodge): requests.Session.send() is actually
164 # reentrant due to how it resolves redirects with nested
165 # calls to send(), however the reentry occurs _after_ the
166 # call to self.get_adapter(), so it doesn't matter that we
167 # will restore _last_get_adapter before a nested send() has
168 # completed as long as we monkeypatch get_adapter() each
169 # time immediately before calling original send() like we
170 # are doing here.
171 _set_method(session, "get_adapter", _fake_get_adapter)
173 # NOTE(jamielennox): self._last_send vs _original_send. Whilst
174 # it seems like here we would use _last_send there is the
175 # possibility that the user has messed up and is somehow
176 # nesting their mockers. If we call last_send at this point
177 # then we end up calling this function again and the outer
178 # level adapter ends up winning. All we really care about here
179 # is that our adapter is in place before calling send so we
180 # always jump directly to the real function so that our most
181 # recently patched send call ends up putting in the most recent
182 # adapter. It feels funny, but it works.
184 try:
185 return _original_send(session, request, **kwargs)
186 except exceptions.NoMockAddress:
187 if not self.real_http:
188 raise
189 except adapter._RunRealHTTP:
190 # this mocker wants you to run the request through the real
191 # requests library rather than the mocking. Let it.
192 pass
193 finally:
194 # restore get_adapter
195 _set_method(session, "get_adapter", self._last_get_adapter)
197 # if we are here it means we must run the real http request
198 # Or, with nested mocks, to the parent mock, that is why we use
199 # _last_send here instead of _original_send
200 if isinstance(self._mock_target, type):
201 return self._last_send(session, request, **kwargs)
202 else:
203 return self._last_send(request, **kwargs)
205 _set_method(self._mock_target, "send", _fake_send)
207 def stop(self):
208 """Stop mocking requests.
210 This should have no impact if mocking has not been started.
211 When nesting mockers, make sure to stop the innermost first.
212 """
213 if self._last_send:
214 self._mock_target.send = self._last_send
215 self._last_send = None
217 # for familiarity with MagicMock
218 def reset_mock(self):
219 self.reset()
221 def __getattr__(self, name):
222 if name in self._PROXY_FUNCS:
223 try:
224 return getattr(self._adapter, name)
225 except AttributeError:
226 pass
228 raise AttributeError(name)
230 def register_uri(self, *args, **kwargs):
231 # you can pass real_http here, but it's private to pass direct to the
232 # adapter, because if you pass direct to the adapter you'll see the exc
233 kwargs['_real_http'] = kwargs.pop('real_http', False)
234 kwargs.setdefault('json_encoder', self._json_encoder)
235 return self._adapter.register_uri(*args, **kwargs)
237 def request(self, *args, **kwargs):
238 return self.register_uri(*args, **kwargs)
240 def get(self, *args, **kwargs):
241 return self.request(GET, *args, **kwargs)
243 def options(self, *args, **kwargs):
244 return self.request(OPTIONS, *args, **kwargs)
246 def head(self, *args, **kwargs):
247 return self.request(HEAD, *args, **kwargs)
249 def post(self, *args, **kwargs):
250 return self.request(POST, *args, **kwargs)
252 def put(self, *args, **kwargs):
253 return self.request(PUT, *args, **kwargs)
255 def patch(self, *args, **kwargs):
256 return self.request(PATCH, *args, **kwargs)
258 def delete(self, *args, **kwargs):
259 return self.request(DELETE, *args, **kwargs)
262class Mocker(MockerCore):
263 """The standard entry point for mock Adapter loading.
264 """
266 #: Defines with what should method name begin to be patched
267 TEST_PREFIX = 'test'
269 def __init__(self, **kwargs):
270 """Create a new mocker adapter.
272 :param str kw: Pass the mock object through to the decorated function
273 as this named keyword argument, rather than a positional argument.
274 :param bool real_http: True to send the request to the real requested
275 uri if there is not a mock installed for it. Defaults to False.
276 """
277 self._kw = kwargs.pop('kw', None)
278 super(Mocker, self).__init__(**kwargs)
280 def __enter__(self):
281 self.start()
282 return self
284 def __exit__(self, type, value, traceback):
285 self.stop()
287 def __call__(self, obj):
288 if isinstance(obj, type):
289 return self.decorate_class(obj)
291 return self.decorate_callable(obj)
293 def copy(self):
294 """Returns an exact copy of current mock
295 """
296 m = type(self)(
297 kw=self._kw,
298 real_http=self.real_http,
299 case_sensitive=self.case_sensitive
300 )
301 return m
303 def decorate_callable(self, func):
304 """Decorates a callable
306 :param callable func: callable to decorate
307 """
308 @functools.wraps(func)
309 def inner(*args, **kwargs):
310 with self.copy() as m:
311 if self._kw:
312 kwargs[self._kw] = m
313 else:
314 args = list(args)
315 args.append(m)
317 return func(*args, **kwargs)
319 return inner
321 def decorate_class(self, klass):
322 """Decorates methods in a class with request_mock
324 Method will be decorated only if it name begins with `TEST_PREFIX`
326 :param object klass: class which methods will be decorated
327 """
328 for attr_name in dir(klass):
329 if not attr_name.startswith(self.TEST_PREFIX):
330 continue
332 attr = getattr(klass, attr_name)
333 if not hasattr(attr, '__call__'):
334 continue
336 m = self.copy()
337 setattr(klass, attr_name, m(attr))
339 return klass
342mock = Mocker