response.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278
  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 io
  13. import http.client
  14. import json as jsonutils
  15. from requests.adapters import HTTPAdapter
  16. from requests.cookies import MockRequest, MockResponse
  17. from requests.cookies import RequestsCookieJar
  18. from requests.cookies import merge_cookies, cookiejar_from_dict
  19. from requests.utils import get_encoding_from_headers
  20. from urllib3.response import HTTPResponse
  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. response.cookies.extract_cookies(MockResponse(response.raw.headers),
  83. MockRequest(request))
  84. # This allows you to pass either a CookieJar or a dictionary to request_uri
  85. # or directly to create_response. To allow more than one cookie to be set.
  86. if cookies:
  87. merge_cookies(response.cookies, cookies)
  88. class _IOReader(io.BytesIO):
  89. """A reader that makes a BytesIO look like a HTTPResponse.
  90. A HTTPResponse will return an empty string when you read from it after
  91. the socket has been closed. A BytesIO will raise a ValueError. For
  92. compatibility we want to do the same thing a HTTPResponse does.
  93. """
  94. def read(self, *args, **kwargs):
  95. if self.closed:
  96. return b''
  97. # if the file is open, but you asked for zero bytes read you should get
  98. # back zero without closing the stream.
  99. if len(args) > 0 and args[0] == 0:
  100. return b''
  101. result = io.BytesIO.read(self, *args, **kwargs)
  102. # when using resp.iter_content(None) it'll go through a different
  103. # request path in urllib3. This path checks whether the object is
  104. # marked closed instead of the return value. see gh124.
  105. if result == b'':
  106. self.close()
  107. return result
  108. def create_response(request, **kwargs):
  109. """
  110. :param int status_code: The status code to return upon a successful
  111. match. Defaults to 200.
  112. :param HTTPResponse raw: A HTTPResponse object to return upon a
  113. successful match.
  114. :param io.IOBase body: An IO object with a read() method that can
  115. return a body on successful match.
  116. :param bytes content: A byte string to return upon a successful match.
  117. :param unicode text: A text string to return upon a successful match.
  118. :param object json: A python object to be converted to a JSON string
  119. and returned upon a successful match.
  120. :param class json_encoder: Encoder object to use for JOSON.
  121. :param dict headers: A dictionary object containing headers that are
  122. returned upon a successful match.
  123. :param CookieJar cookies: A cookie jar with cookies to set on the
  124. response.
  125. :returns requests.Response: A response object that can
  126. be returned to requests.
  127. """
  128. connection = kwargs.pop('connection', _FakeConnection())
  129. _check_body_arguments(**kwargs)
  130. raw = kwargs.pop('raw', None)
  131. body = kwargs.pop('body', None)
  132. content = kwargs.pop('content', None)
  133. text = kwargs.pop('text', None)
  134. json = kwargs.pop('json', None)
  135. headers = kwargs.pop('headers', {})
  136. encoding = None
  137. if content is not None and not isinstance(content, bytes):
  138. raise TypeError('Content should be binary data')
  139. if text is not None and not isinstance(text, str):
  140. raise TypeError('Text should be string data')
  141. if json is not None:
  142. encoder = kwargs.pop('json_encoder', None) or jsonutils.JSONEncoder
  143. text = jsonutils.dumps(json, cls=encoder)
  144. if text is not None:
  145. encoding = get_encoding_from_headers(headers) or 'utf-8'
  146. content = text.encode(encoding)
  147. if content is not None:
  148. body = _IOReader(content)
  149. if not raw:
  150. status = kwargs.get('status_code', _DEFAULT_STATUS)
  151. reason = kwargs.get('reason', http.client.responses.get(status))
  152. raw = HTTPResponse(status=status,
  153. reason=reason,
  154. headers=headers,
  155. body=body or _IOReader(b''),
  156. decode_content=False,
  157. enforce_content_length=False,
  158. preload_content=False,
  159. original_response=None)
  160. response = _http_adapter.build_response(request, raw)
  161. response.connection = connection
  162. if encoding and not response.encoding:
  163. response.encoding = encoding
  164. _extract_cookies(request, response, kwargs.get('cookies'))
  165. return response
  166. class _Context(object):
  167. """Stores the data being used to process a current URL match."""
  168. def __init__(self, headers, status_code, reason, cookies):
  169. self.headers = headers
  170. self.status_code = status_code
  171. self.reason = reason
  172. self.cookies = cookies
  173. class _MatcherResponse(object):
  174. def __init__(self, **kwargs):
  175. self._exc = kwargs.pop('exc', None)
  176. # If the user is asking for an exception to be thrown then prevent them
  177. # specifying any sort of body or status response as it won't be used.
  178. # This may be protecting the user too much but can be removed later.
  179. if self._exc and kwargs:
  180. raise TypeError('Cannot provide other arguments with exc.')
  181. _check_body_arguments(**kwargs)
  182. self._params = kwargs
  183. # whilst in general you shouldn't do type checking in python this
  184. # makes sure we don't end up with differences between the way types
  185. # are handled between python 2 and 3.
  186. content = self._params.get('content')
  187. text = self._params.get('text')
  188. if content is not None and not (callable(content) or
  189. isinstance(content, bytes)):
  190. raise TypeError('Content should be a callback or binary data')
  191. if text is not None and not (callable(text) or
  192. isinstance(text, str)):
  193. raise TypeError('Text should be a callback or string data')
  194. def get_response(self, request):
  195. # if an error was requested then raise that instead of doing response
  196. if self._exc:
  197. raise self._exc
  198. # If a cookie dict is passed convert it into a CookieJar so that the
  199. # cookies object available in a callback context is always a jar.
  200. cookies = self._params.get('cookies', CookieJar())
  201. if isinstance(cookies, dict):
  202. cookies = cookiejar_from_dict(cookies, CookieJar())
  203. context = _Context(self._params.get('headers', {}).copy(),
  204. self._params.get('status_code', _DEFAULT_STATUS),
  205. self._params.get('reason'),
  206. cookies)
  207. # if a body element is a callback then execute it
  208. def _call(f, *args, **kwargs):
  209. return f(request, context, *args, **kwargs) if callable(f) else f
  210. return create_response(request,
  211. json=_call(self._params.get('json')),
  212. text=_call(self._params.get('text')),
  213. content=_call(self._params.get('content')),
  214. body=_call(self._params.get('body')),
  215. raw=_call(self._params.get('raw')),
  216. json_encoder=self._params.get('json_encoder'),
  217. status_code=context.status_code,
  218. reason=context.reason,
  219. headers=context.headers,
  220. cookies=context.cookies)