response.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281
  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.
  12. import json as jsonutils
  13. from requests.adapters import HTTPAdapter
  14. from requests.cookies import MockRequest, MockResponse
  15. from requests.cookies import RequestsCookieJar
  16. from requests.cookies import merge_cookies, cookiejar_from_dict
  17. from requests.packages.urllib3.response import HTTPResponse
  18. from requests.utils import get_encoding_from_headers
  19. import six
  20. from requests_mock import compat
  21. from requests_mock import exceptions
  22. _BODY_ARGS = frozenset(['raw', 'body', 'content', 'text', 'json'])
  23. _HTTP_ARGS = frozenset([
  24. 'status_code',
  25. 'reason',
  26. 'headers',
  27. 'cookies',
  28. 'json_encoder',
  29. ])
  30. _DEFAULT_STATUS = 200
  31. _http_adapter = HTTPAdapter()
  32. class CookieJar(RequestsCookieJar):
  33. def set(self, name, value, **kwargs):
  34. """Add a cookie to the Jar.
  35. :param str name: cookie name/key.
  36. :param str value: cookie value.
  37. :param int version: Integer or None. Netscape cookies have version 0.
  38. RFC 2965 and RFC 2109 cookies have a version cookie-attribute of 1.
  39. However, note that cookielib may 'downgrade' RFC 2109 cookies to
  40. Netscape cookies, in which case version is 0.
  41. :param str port: String representing a port or a set of ports
  42. (eg. '80', or '80,8080'),
  43. :param str domain: The domain the cookie should apply to.
  44. :param str path: Cookie path (a string, eg. '/acme/rocket_launchers').
  45. :param bool secure: True if cookie should only be returned over a
  46. secure connection.
  47. :param int expires: Integer expiry date in seconds since epoch or None.
  48. :param bool discard: True if this is a session cookie.
  49. :param str comment: String comment from the server explaining the
  50. function of this cookie.
  51. :param str comment_url: URL linking to a comment from the server
  52. explaining the function of this cookie.
  53. """
  54. # just here to provide the function documentation
  55. return super(CookieJar, self).set(name, value, **kwargs)
  56. def _check_body_arguments(**kwargs):
  57. # mutual exclusion, only 1 body method may be provided
  58. provided = [x for x in _BODY_ARGS if kwargs.pop(x, None) is not None]
  59. if len(provided) > 1:
  60. raise RuntimeError('You may only supply one body element. You '
  61. 'supplied %s' % ', '.join(provided))
  62. extra = [x for x in kwargs if x not in _HTTP_ARGS]
  63. if extra:
  64. raise TypeError('Too many arguments provided. Unexpected '
  65. 'arguments %s.' % ', '.join(extra))
  66. class _FakeConnection(object):
  67. """An object that can mock the necessary parts of a socket interface."""
  68. def send(self, request, **kwargs):
  69. msg = 'This response was created without a connection. You are ' \
  70. 'therefore unable to make a request directly on that connection.'
  71. raise exceptions.InvalidRequest(msg)
  72. def close(self):
  73. pass
  74. def _extract_cookies(request, response, cookies):
  75. """Add cookies to the response.
  76. Cookies in requests are extracted from the headers in the original_response
  77. httplib.HTTPMessage which we don't create so we have to do this step
  78. manually.
  79. """
  80. # This will add cookies set manually via the Set-Cookie or Set-Cookie2
  81. # header but this only allows 1 cookie to be set.
  82. http_message = compat._FakeHTTPMessage(response.headers)
  83. response.cookies.extract_cookies(MockResponse(http_message),
  84. MockRequest(request))
  85. # This allows you to pass either a CookieJar or a dictionary to request_uri
  86. # or directly to create_response. To allow more than one cookie to be set.
  87. if cookies:
  88. merge_cookies(response.cookies, cookies)
  89. class _IOReader(six.BytesIO):
  90. """A reader that makes a BytesIO look like a HTTPResponse.
  91. A HTTPResponse will return an empty string when you read from it after
  92. the socket has been closed. A BytesIO will raise a ValueError. For
  93. compatibility we want to do the same thing a HTTPResponse does.
  94. """
  95. def read(self, *args, **kwargs):
  96. if self.closed:
  97. return six.b('')
  98. # if the file is open, but you asked for zero bytes read you should get
  99. # back zero without closing the stream.
  100. if len(args) > 0 and args[0] == 0:
  101. return six.b('')
  102. # not a new style object in python 2
  103. result = six.BytesIO.read(self, *args, **kwargs)
  104. # when using resp.iter_content(None) it'll go through a different
  105. # request path in urllib3. This path checks whether the object is
  106. # marked closed instead of the return value. see gh124.
  107. if result == six.b(''):
  108. self.close()
  109. return result
  110. def create_response(request, **kwargs):
  111. """
  112. :param int status_code: The status code to return upon a successful
  113. match. Defaults to 200.
  114. :param HTTPResponse raw: A HTTPResponse object to return upon a
  115. successful match.
  116. :param io.IOBase body: An IO object with a read() method that can
  117. return a body on successful match.
  118. :param bytes content: A byte string to return upon a successful match.
  119. :param unicode text: A text string to return upon a successful match.
  120. :param object json: A python object to be converted to a JSON string
  121. and returned upon a successful match.
  122. :param class json_encoder: Encoder object to use for JOSON.
  123. :param dict headers: A dictionary object containing headers that are
  124. returned upon a successful match.
  125. :param CookieJar cookies: A cookie jar with cookies to set on the
  126. response.
  127. :returns requests.Response: A response object that can
  128. be returned to requests.
  129. """
  130. connection = kwargs.pop('connection', _FakeConnection())
  131. _check_body_arguments(**kwargs)
  132. raw = kwargs.pop('raw', None)
  133. body = kwargs.pop('body', None)
  134. content = kwargs.pop('content', None)
  135. text = kwargs.pop('text', None)
  136. json = kwargs.pop('json', None)
  137. headers = kwargs.pop('headers', {})
  138. encoding = None
  139. if content is not None and not isinstance(content, six.binary_type):
  140. raise TypeError('Content should be binary data')
  141. if text is not None and not isinstance(text, six.string_types):
  142. raise TypeError('Text should be string data')
  143. if json is not None:
  144. encoder = kwargs.pop('json_encoder', None) or jsonutils.JSONEncoder
  145. text = jsonutils.dumps(json, cls=encoder)
  146. if text is not None:
  147. encoding = get_encoding_from_headers(headers) or 'utf-8'
  148. content = text.encode(encoding)
  149. if content is not None:
  150. body = _IOReader(content)
  151. if not raw:
  152. status = kwargs.get('status_code', _DEFAULT_STATUS)
  153. reason = kwargs.get('reason',
  154. six.moves.http_client.responses.get(status))
  155. raw = HTTPResponse(status=status,
  156. reason=reason,
  157. headers=headers,
  158. body=body or _IOReader(six.b('')),
  159. decode_content=False,
  160. enforce_content_length=False,
  161. preload_content=False,
  162. original_response=None)
  163. response = _http_adapter.build_response(request, raw)
  164. response.connection = connection
  165. if encoding and not response.encoding:
  166. response.encoding = encoding
  167. _extract_cookies(request, response, kwargs.get('cookies'))
  168. return response
  169. class _Context(object):
  170. """Stores the data being used to process a current URL match."""
  171. def __init__(self, headers, status_code, reason, cookies):
  172. self.headers = headers
  173. self.status_code = status_code
  174. self.reason = reason
  175. self.cookies = cookies
  176. class _MatcherResponse(object):
  177. def __init__(self, **kwargs):
  178. self._exc = kwargs.pop('exc', None)
  179. # If the user is asking for an exception to be thrown then prevent them
  180. # specifying any sort of body or status response as it won't be used.
  181. # This may be protecting the user too much but can be removed later.
  182. if self._exc and kwargs:
  183. raise TypeError('Cannot provide other arguments with exc.')
  184. _check_body_arguments(**kwargs)
  185. self._params = kwargs
  186. # whilst in general you shouldn't do type checking in python this
  187. # makes sure we don't end up with differences between the way types
  188. # are handled between python 2 and 3.
  189. content = self._params.get('content')
  190. text = self._params.get('text')
  191. if content is not None and not (callable(content) or
  192. isinstance(content, six.binary_type)):
  193. raise TypeError('Content should be a callback or binary data')
  194. if text is not None and not (callable(text) or
  195. isinstance(text, six.string_types)):
  196. raise TypeError('Text should be a callback or string data')
  197. def get_response(self, request):
  198. # if an error was requested then raise that instead of doing response
  199. if self._exc:
  200. raise self._exc
  201. # If a cookie dict is passed convert it into a CookieJar so that the
  202. # cookies object available in a callback context is always a jar.
  203. cookies = self._params.get('cookies', CookieJar())
  204. if isinstance(cookies, dict):
  205. cookies = cookiejar_from_dict(cookies, CookieJar())
  206. context = _Context(self._params.get('headers', {}).copy(),
  207. self._params.get('status_code', _DEFAULT_STATUS),
  208. self._params.get('reason'),
  209. cookies)
  210. # if a body element is a callback then execute it
  211. def _call(f, *args, **kwargs):
  212. return f(request, context, *args, **kwargs) if callable(f) else f
  213. return create_response(request,
  214. json=_call(self._params.get('json')),
  215. text=_call(self._params.get('text')),
  216. content=_call(self._params.get('content')),
  217. body=_call(self._params.get('body')),
  218. raw=self._params.get('raw'),
  219. json_encoder=self._params.get('json_encoder'),
  220. status_code=context.status_code,
  221. reason=context.reason,
  222. headers=context.headers,
  223. cookies=context.cookies)