test.py 51 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464
  1. from __future__ import annotations
  2. import dataclasses
  3. import mimetypes
  4. import sys
  5. import typing as t
  6. from collections import defaultdict
  7. from datetime import datetime
  8. from io import BytesIO
  9. from itertools import chain
  10. from random import random
  11. from tempfile import TemporaryFile
  12. from time import time
  13. from urllib.parse import unquote
  14. from urllib.parse import urlsplit
  15. from urllib.parse import urlunsplit
  16. from ._internal import _get_environ
  17. from ._internal import _wsgi_decoding_dance
  18. from ._internal import _wsgi_encoding_dance
  19. from .datastructures import Authorization
  20. from .datastructures import CallbackDict
  21. from .datastructures import CombinedMultiDict
  22. from .datastructures import EnvironHeaders
  23. from .datastructures import FileMultiDict
  24. from .datastructures import Headers
  25. from .datastructures import MultiDict
  26. from .http import dump_cookie
  27. from .http import dump_options_header
  28. from .http import parse_cookie
  29. from .http import parse_date
  30. from .http import parse_options_header
  31. from .sansio.multipart import Data
  32. from .sansio.multipart import Epilogue
  33. from .sansio.multipart import Field
  34. from .sansio.multipart import File
  35. from .sansio.multipart import MultipartEncoder
  36. from .sansio.multipart import Preamble
  37. from .urls import _urlencode
  38. from .urls import iri_to_uri
  39. from .utils import cached_property
  40. from .utils import get_content_type
  41. from .wrappers.request import Request
  42. from .wrappers.response import Response
  43. from .wsgi import ClosingIterator
  44. from .wsgi import get_current_url
  45. if t.TYPE_CHECKING:
  46. from _typeshed.wsgi import WSGIApplication
  47. from _typeshed.wsgi import WSGIEnvironment
  48. import typing_extensions as te
  49. def stream_encode_multipart(
  50. data: t.Mapping[str, t.Any],
  51. use_tempfile: bool = True,
  52. threshold: int = 1024 * 500,
  53. boundary: str | None = None,
  54. ) -> tuple[t.IO[bytes], int, str]:
  55. """Encode a dict of values (either strings or file descriptors or
  56. :class:`FileStorage` objects.) into a multipart encoded string stored
  57. in a file descriptor.
  58. .. versionchanged:: 3.0
  59. The ``charset`` parameter was removed.
  60. """
  61. if boundary is None:
  62. boundary = f"---------------WerkzeugFormPart_{time()}{random()}"
  63. stream: t.IO[bytes] = BytesIO()
  64. total_length = 0
  65. on_disk = False
  66. write_binary: t.Callable[[bytes], int]
  67. if use_tempfile:
  68. def write_binary(s: bytes) -> int:
  69. nonlocal stream, total_length, on_disk
  70. if on_disk:
  71. return stream.write(s)
  72. else:
  73. length = len(s)
  74. if length + total_length <= threshold:
  75. stream.write(s)
  76. else:
  77. new_stream = t.cast(t.IO[bytes], TemporaryFile("wb+"))
  78. new_stream.write(stream.getvalue()) # type: ignore
  79. new_stream.write(s)
  80. stream = new_stream
  81. on_disk = True
  82. total_length += length
  83. return length
  84. else:
  85. write_binary = stream.write
  86. encoder = MultipartEncoder(boundary.encode())
  87. write_binary(encoder.send_event(Preamble(data=b"")))
  88. for key, value in _iter_data(data):
  89. reader = getattr(value, "read", None)
  90. if reader is not None:
  91. filename = getattr(value, "filename", getattr(value, "name", None))
  92. content_type = getattr(value, "content_type", None)
  93. if content_type is None:
  94. content_type = (
  95. filename
  96. and mimetypes.guess_type(filename)[0]
  97. or "application/octet-stream"
  98. )
  99. headers = value.headers
  100. headers.update([("Content-Type", content_type)])
  101. if filename is None:
  102. write_binary(encoder.send_event(Field(name=key, headers=headers)))
  103. else:
  104. write_binary(
  105. encoder.send_event(
  106. File(name=key, filename=filename, headers=headers)
  107. )
  108. )
  109. while True:
  110. chunk = reader(16384)
  111. if not chunk:
  112. write_binary(encoder.send_event(Data(data=chunk, more_data=False)))
  113. break
  114. write_binary(encoder.send_event(Data(data=chunk, more_data=True)))
  115. else:
  116. if not isinstance(value, str):
  117. value = str(value)
  118. write_binary(encoder.send_event(Field(name=key, headers=Headers())))
  119. write_binary(encoder.send_event(Data(data=value.encode(), more_data=False)))
  120. write_binary(encoder.send_event(Epilogue(data=b"")))
  121. length = stream.tell()
  122. stream.seek(0)
  123. return stream, length, boundary
  124. def encode_multipart(
  125. values: t.Mapping[str, t.Any], boundary: str | None = None
  126. ) -> tuple[str, bytes]:
  127. """Like `stream_encode_multipart` but returns a tuple in the form
  128. (``boundary``, ``data``) where data is bytes.
  129. .. versionchanged:: 3.0
  130. The ``charset`` parameter was removed.
  131. """
  132. stream, length, boundary = stream_encode_multipart(
  133. values, use_tempfile=False, boundary=boundary
  134. )
  135. return boundary, stream.read()
  136. def _iter_data(data: t.Mapping[str, t.Any]) -> t.Iterator[tuple[str, t.Any]]:
  137. """Iterate over a mapping that might have a list of values, yielding
  138. all key, value pairs. Almost like iter_multi_items but only allows
  139. lists, not tuples, of values so tuples can be used for files.
  140. """
  141. if isinstance(data, MultiDict):
  142. yield from data.items(multi=True)
  143. else:
  144. for key, value in data.items():
  145. if isinstance(value, list):
  146. for v in value:
  147. yield key, v
  148. else:
  149. yield key, value
  150. _TAnyMultiDict = t.TypeVar("_TAnyMultiDict", bound=MultiDict)
  151. class EnvironBuilder:
  152. """This class can be used to conveniently create a WSGI environment
  153. for testing purposes. It can be used to quickly create WSGI environments
  154. or request objects from arbitrary data.
  155. The signature of this class is also used in some other places as of
  156. Werkzeug 0.5 (:func:`create_environ`, :meth:`Response.from_values`,
  157. :meth:`Client.open`). Because of this most of the functionality is
  158. available through the constructor alone.
  159. Files and regular form data can be manipulated independently of each
  160. other with the :attr:`form` and :attr:`files` attributes, but are
  161. passed with the same argument to the constructor: `data`.
  162. `data` can be any of these values:
  163. - a `str` or `bytes` object: The object is converted into an
  164. :attr:`input_stream`, the :attr:`content_length` is set and you have to
  165. provide a :attr:`content_type`.
  166. - a `dict` or :class:`MultiDict`: The keys have to be strings. The values
  167. have to be either any of the following objects, or a list of any of the
  168. following objects:
  169. - a :class:`file`-like object: These are converted into
  170. :class:`FileStorage` objects automatically.
  171. - a `tuple`: The :meth:`~FileMultiDict.add_file` method is called
  172. with the key and the unpacked `tuple` items as positional
  173. arguments.
  174. - a `str`: The string is set as form data for the associated key.
  175. - a file-like object: The object content is loaded in memory and then
  176. handled like a regular `str` or a `bytes`.
  177. :param path: the path of the request. In the WSGI environment this will
  178. end up as `PATH_INFO`. If the `query_string` is not defined
  179. and there is a question mark in the `path` everything after
  180. it is used as query string.
  181. :param base_url: the base URL is a URL that is used to extract the WSGI
  182. URL scheme, host (server name + server port) and the
  183. script root (`SCRIPT_NAME`).
  184. :param query_string: an optional string or dict with URL parameters.
  185. :param method: the HTTP method to use, defaults to `GET`.
  186. :param input_stream: an optional input stream. Do not specify this and
  187. `data`. As soon as an input stream is set you can't
  188. modify :attr:`args` and :attr:`files` unless you
  189. set the :attr:`input_stream` to `None` again.
  190. :param content_type: The content type for the request. As of 0.5 you
  191. don't have to provide this when specifying files
  192. and form data via `data`.
  193. :param content_length: The content length for the request. You don't
  194. have to specify this when providing data via
  195. `data`.
  196. :param errors_stream: an optional error stream that is used for
  197. `wsgi.errors`. Defaults to :data:`stderr`.
  198. :param multithread: controls `wsgi.multithread`. Defaults to `False`.
  199. :param multiprocess: controls `wsgi.multiprocess`. Defaults to `False`.
  200. :param run_once: controls `wsgi.run_once`. Defaults to `False`.
  201. :param headers: an optional list or :class:`Headers` object of headers.
  202. :param data: a string or dict of form data or a file-object.
  203. See explanation above.
  204. :param json: An object to be serialized and assigned to ``data``.
  205. Defaults the content type to ``"application/json"``.
  206. Serialized with the function assigned to :attr:`json_dumps`.
  207. :param environ_base: an optional dict of environment defaults.
  208. :param environ_overrides: an optional dict of environment overrides.
  209. :param auth: An authorization object to use for the
  210. ``Authorization`` header value. A ``(username, password)`` tuple
  211. is a shortcut for ``Basic`` authorization.
  212. .. versionchanged:: 3.0
  213. The ``charset`` parameter was removed.
  214. .. versionchanged:: 2.1
  215. ``CONTENT_TYPE`` and ``CONTENT_LENGTH`` are not duplicated as
  216. header keys in the environ.
  217. .. versionchanged:: 2.0
  218. ``REQUEST_URI`` and ``RAW_URI`` is the full raw URI including
  219. the query string, not only the path.
  220. .. versionchanged:: 2.0
  221. The default :attr:`request_class` is ``Request`` instead of
  222. ``BaseRequest``.
  223. .. versionadded:: 2.0
  224. Added the ``auth`` parameter.
  225. .. versionadded:: 0.15
  226. The ``json`` param and :meth:`json_dumps` method.
  227. .. versionadded:: 0.15
  228. The environ has keys ``REQUEST_URI`` and ``RAW_URI`` containing
  229. the path before percent-decoding. This is not part of the WSGI
  230. PEP, but many WSGI servers include it.
  231. .. versionchanged:: 0.6
  232. ``path`` and ``base_url`` can now be unicode strings that are
  233. encoded with :func:`iri_to_uri`.
  234. """
  235. #: the server protocol to use. defaults to HTTP/1.1
  236. server_protocol = "HTTP/1.1"
  237. #: the wsgi version to use. defaults to (1, 0)
  238. wsgi_version = (1, 0)
  239. #: The default request class used by :meth:`get_request`.
  240. request_class = Request
  241. import json
  242. #: The serialization function used when ``json`` is passed.
  243. json_dumps = staticmethod(json.dumps)
  244. del json
  245. _args: MultiDict | None
  246. _query_string: str | None
  247. _input_stream: t.IO[bytes] | None
  248. _form: MultiDict | None
  249. _files: FileMultiDict | None
  250. def __init__(
  251. self,
  252. path: str = "/",
  253. base_url: str | None = None,
  254. query_string: t.Mapping[str, str] | str | None = None,
  255. method: str = "GET",
  256. input_stream: t.IO[bytes] | None = None,
  257. content_type: str | None = None,
  258. content_length: int | None = None,
  259. errors_stream: t.IO[str] | None = None,
  260. multithread: bool = False,
  261. multiprocess: bool = False,
  262. run_once: bool = False,
  263. headers: Headers | t.Iterable[tuple[str, str]] | None = None,
  264. data: None | (t.IO[bytes] | str | bytes | t.Mapping[str, t.Any]) = None,
  265. environ_base: t.Mapping[str, t.Any] | None = None,
  266. environ_overrides: t.Mapping[str, t.Any] | None = None,
  267. mimetype: str | None = None,
  268. json: t.Mapping[str, t.Any] | None = None,
  269. auth: Authorization | tuple[str, str] | None = None,
  270. ) -> None:
  271. if query_string is not None and "?" in path:
  272. raise ValueError("Query string is defined in the path and as an argument")
  273. request_uri = urlsplit(path)
  274. if query_string is None and "?" in path:
  275. query_string = request_uri.query
  276. self.path = iri_to_uri(request_uri.path)
  277. self.request_uri = path
  278. if base_url is not None:
  279. base_url = iri_to_uri(base_url)
  280. self.base_url = base_url # type: ignore
  281. if isinstance(query_string, str):
  282. self.query_string = query_string
  283. else:
  284. if query_string is None:
  285. query_string = MultiDict()
  286. elif not isinstance(query_string, MultiDict):
  287. query_string = MultiDict(query_string)
  288. self.args = query_string
  289. self.method = method
  290. if headers is None:
  291. headers = Headers()
  292. elif not isinstance(headers, Headers):
  293. headers = Headers(headers)
  294. self.headers = headers
  295. if content_type is not None:
  296. self.content_type = content_type
  297. if errors_stream is None:
  298. errors_stream = sys.stderr
  299. self.errors_stream = errors_stream
  300. self.multithread = multithread
  301. self.multiprocess = multiprocess
  302. self.run_once = run_once
  303. self.environ_base = environ_base
  304. self.environ_overrides = environ_overrides
  305. self.input_stream = input_stream
  306. self.content_length = content_length
  307. self.closed = False
  308. if auth is not None:
  309. if isinstance(auth, tuple):
  310. auth = Authorization(
  311. "basic", {"username": auth[0], "password": auth[1]}
  312. )
  313. self.headers.set("Authorization", auth.to_header())
  314. if json is not None:
  315. if data is not None:
  316. raise TypeError("can't provide both json and data")
  317. data = self.json_dumps(json)
  318. if self.content_type is None:
  319. self.content_type = "application/json"
  320. if data:
  321. if input_stream is not None:
  322. raise TypeError("can't provide input stream and data")
  323. if hasattr(data, "read"):
  324. data = data.read()
  325. if isinstance(data, str):
  326. data = data.encode()
  327. if isinstance(data, bytes):
  328. self.input_stream = BytesIO(data)
  329. if self.content_length is None:
  330. self.content_length = len(data)
  331. else:
  332. for key, value in _iter_data(data):
  333. if isinstance(value, (tuple, dict)) or hasattr(value, "read"):
  334. self._add_file_from_data(key, value)
  335. else:
  336. self.form.setlistdefault(key).append(value)
  337. if mimetype is not None:
  338. self.mimetype = mimetype
  339. @classmethod
  340. def from_environ(cls, environ: WSGIEnvironment, **kwargs: t.Any) -> EnvironBuilder:
  341. """Turn an environ dict back into a builder. Any extra kwargs
  342. override the args extracted from the environ.
  343. .. versionchanged:: 2.0
  344. Path and query values are passed through the WSGI decoding
  345. dance to avoid double encoding.
  346. .. versionadded:: 0.15
  347. """
  348. headers = Headers(EnvironHeaders(environ))
  349. out = {
  350. "path": _wsgi_decoding_dance(environ["PATH_INFO"]),
  351. "base_url": cls._make_base_url(
  352. environ["wsgi.url_scheme"],
  353. headers.pop("Host"),
  354. _wsgi_decoding_dance(environ["SCRIPT_NAME"]),
  355. ),
  356. "query_string": _wsgi_decoding_dance(environ["QUERY_STRING"]),
  357. "method": environ["REQUEST_METHOD"],
  358. "input_stream": environ["wsgi.input"],
  359. "content_type": headers.pop("Content-Type", None),
  360. "content_length": headers.pop("Content-Length", None),
  361. "errors_stream": environ["wsgi.errors"],
  362. "multithread": environ["wsgi.multithread"],
  363. "multiprocess": environ["wsgi.multiprocess"],
  364. "run_once": environ["wsgi.run_once"],
  365. "headers": headers,
  366. }
  367. out.update(kwargs)
  368. return cls(**out)
  369. def _add_file_from_data(
  370. self,
  371. key: str,
  372. value: (t.IO[bytes] | tuple[t.IO[bytes], str] | tuple[t.IO[bytes], str, str]),
  373. ) -> None:
  374. """Called in the EnvironBuilder to add files from the data dict."""
  375. if isinstance(value, tuple):
  376. self.files.add_file(key, *value)
  377. else:
  378. self.files.add_file(key, value)
  379. @staticmethod
  380. def _make_base_url(scheme: str, host: str, script_root: str) -> str:
  381. return urlunsplit((scheme, host, script_root, "", "")).rstrip("/") + "/"
  382. @property
  383. def base_url(self) -> str:
  384. """The base URL is used to extract the URL scheme, host name,
  385. port, and root path.
  386. """
  387. return self._make_base_url(self.url_scheme, self.host, self.script_root)
  388. @base_url.setter
  389. def base_url(self, value: str | None) -> None:
  390. if value is None:
  391. scheme = "http"
  392. netloc = "localhost"
  393. script_root = ""
  394. else:
  395. scheme, netloc, script_root, qs, anchor = urlsplit(value)
  396. if qs or anchor:
  397. raise ValueError("base url must not contain a query string or fragment")
  398. self.script_root = script_root.rstrip("/")
  399. self.host = netloc
  400. self.url_scheme = scheme
  401. @property
  402. def content_type(self) -> str | None:
  403. """The content type for the request. Reflected from and to
  404. the :attr:`headers`. Do not set if you set :attr:`files` or
  405. :attr:`form` for auto detection.
  406. """
  407. ct = self.headers.get("Content-Type")
  408. if ct is None and not self._input_stream:
  409. if self._files:
  410. return "multipart/form-data"
  411. if self._form:
  412. return "application/x-www-form-urlencoded"
  413. return None
  414. return ct
  415. @content_type.setter
  416. def content_type(self, value: str | None) -> None:
  417. if value is None:
  418. self.headers.pop("Content-Type", None)
  419. else:
  420. self.headers["Content-Type"] = value
  421. @property
  422. def mimetype(self) -> str | None:
  423. """The mimetype (content type without charset etc.)
  424. .. versionadded:: 0.14
  425. """
  426. ct = self.content_type
  427. return ct.split(";")[0].strip() if ct else None
  428. @mimetype.setter
  429. def mimetype(self, value: str) -> None:
  430. self.content_type = get_content_type(value, "utf-8")
  431. @property
  432. def mimetype_params(self) -> t.Mapping[str, str]:
  433. """The mimetype parameters as dict. For example if the
  434. content type is ``text/html; charset=utf-8`` the params would be
  435. ``{'charset': 'utf-8'}``.
  436. .. versionadded:: 0.14
  437. """
  438. def on_update(d: CallbackDict) -> None:
  439. self.headers["Content-Type"] = dump_options_header(self.mimetype, d)
  440. d = parse_options_header(self.headers.get("content-type", ""))[1]
  441. return CallbackDict(d, on_update)
  442. @property
  443. def content_length(self) -> int | None:
  444. """The content length as integer. Reflected from and to the
  445. :attr:`headers`. Do not set if you set :attr:`files` or
  446. :attr:`form` for auto detection.
  447. """
  448. return self.headers.get("Content-Length", type=int)
  449. @content_length.setter
  450. def content_length(self, value: int | None) -> None:
  451. if value is None:
  452. self.headers.pop("Content-Length", None)
  453. else:
  454. self.headers["Content-Length"] = str(value)
  455. def _get_form(self, name: str, storage: type[_TAnyMultiDict]) -> _TAnyMultiDict:
  456. """Common behavior for getting the :attr:`form` and
  457. :attr:`files` properties.
  458. :param name: Name of the internal cached attribute.
  459. :param storage: Storage class used for the data.
  460. """
  461. if self.input_stream is not None:
  462. raise AttributeError("an input stream is defined")
  463. rv = getattr(self, name)
  464. if rv is None:
  465. rv = storage()
  466. setattr(self, name, rv)
  467. return rv # type: ignore
  468. def _set_form(self, name: str, value: MultiDict) -> None:
  469. """Common behavior for setting the :attr:`form` and
  470. :attr:`files` properties.
  471. :param name: Name of the internal cached attribute.
  472. :param value: Value to assign to the attribute.
  473. """
  474. self._input_stream = None
  475. setattr(self, name, value)
  476. @property
  477. def form(self) -> MultiDict:
  478. """A :class:`MultiDict` of form values."""
  479. return self._get_form("_form", MultiDict)
  480. @form.setter
  481. def form(self, value: MultiDict) -> None:
  482. self._set_form("_form", value)
  483. @property
  484. def files(self) -> FileMultiDict:
  485. """A :class:`FileMultiDict` of uploaded files. Use
  486. :meth:`~FileMultiDict.add_file` to add new files.
  487. """
  488. return self._get_form("_files", FileMultiDict)
  489. @files.setter
  490. def files(self, value: FileMultiDict) -> None:
  491. self._set_form("_files", value)
  492. @property
  493. def input_stream(self) -> t.IO[bytes] | None:
  494. """An optional input stream. This is mutually exclusive with
  495. setting :attr:`form` and :attr:`files`, setting it will clear
  496. those. Do not provide this if the method is not ``POST`` or
  497. another method that has a body.
  498. """
  499. return self._input_stream
  500. @input_stream.setter
  501. def input_stream(self, value: t.IO[bytes] | None) -> None:
  502. self._input_stream = value
  503. self._form = None
  504. self._files = None
  505. @property
  506. def query_string(self) -> str:
  507. """The query string. If you set this to a string
  508. :attr:`args` will no longer be available.
  509. """
  510. if self._query_string is None:
  511. if self._args is not None:
  512. return _urlencode(self._args)
  513. return ""
  514. return self._query_string
  515. @query_string.setter
  516. def query_string(self, value: str | None) -> None:
  517. self._query_string = value
  518. self._args = None
  519. @property
  520. def args(self) -> MultiDict:
  521. """The URL arguments as :class:`MultiDict`."""
  522. if self._query_string is not None:
  523. raise AttributeError("a query string is defined")
  524. if self._args is None:
  525. self._args = MultiDict()
  526. return self._args
  527. @args.setter
  528. def args(self, value: MultiDict | None) -> None:
  529. self._query_string = None
  530. self._args = value
  531. @property
  532. def server_name(self) -> str:
  533. """The server name (read-only, use :attr:`host` to set)"""
  534. return self.host.split(":", 1)[0]
  535. @property
  536. def server_port(self) -> int:
  537. """The server port as integer (read-only, use :attr:`host` to set)"""
  538. pieces = self.host.split(":", 1)
  539. if len(pieces) == 2:
  540. try:
  541. return int(pieces[1])
  542. except ValueError:
  543. pass
  544. if self.url_scheme == "https":
  545. return 443
  546. return 80
  547. def __del__(self) -> None:
  548. try:
  549. self.close()
  550. except Exception:
  551. pass
  552. def close(self) -> None:
  553. """Closes all files. If you put real :class:`file` objects into the
  554. :attr:`files` dict you can call this method to automatically close
  555. them all in one go.
  556. """
  557. if self.closed:
  558. return
  559. try:
  560. files = self.files.values()
  561. except AttributeError:
  562. files = () # type: ignore
  563. for f in files:
  564. try:
  565. f.close()
  566. except Exception:
  567. pass
  568. self.closed = True
  569. def get_environ(self) -> WSGIEnvironment:
  570. """Return the built environ.
  571. .. versionchanged:: 0.15
  572. The content type and length headers are set based on
  573. input stream detection. Previously this only set the WSGI
  574. keys.
  575. """
  576. input_stream = self.input_stream
  577. content_length = self.content_length
  578. mimetype = self.mimetype
  579. content_type = self.content_type
  580. if input_stream is not None:
  581. start_pos = input_stream.tell()
  582. input_stream.seek(0, 2)
  583. end_pos = input_stream.tell()
  584. input_stream.seek(start_pos)
  585. content_length = end_pos - start_pos
  586. elif mimetype == "multipart/form-data":
  587. input_stream, content_length, boundary = stream_encode_multipart(
  588. CombinedMultiDict([self.form, self.files])
  589. )
  590. content_type = f'{mimetype}; boundary="{boundary}"'
  591. elif mimetype == "application/x-www-form-urlencoded":
  592. form_encoded = _urlencode(self.form).encode("ascii")
  593. content_length = len(form_encoded)
  594. input_stream = BytesIO(form_encoded)
  595. else:
  596. input_stream = BytesIO()
  597. result: WSGIEnvironment = {}
  598. if self.environ_base:
  599. result.update(self.environ_base)
  600. def _path_encode(x: str) -> str:
  601. return _wsgi_encoding_dance(unquote(x))
  602. raw_uri = _wsgi_encoding_dance(self.request_uri)
  603. result.update(
  604. {
  605. "REQUEST_METHOD": self.method,
  606. "SCRIPT_NAME": _path_encode(self.script_root),
  607. "PATH_INFO": _path_encode(self.path),
  608. "QUERY_STRING": _wsgi_encoding_dance(self.query_string),
  609. # Non-standard, added by mod_wsgi, uWSGI
  610. "REQUEST_URI": raw_uri,
  611. # Non-standard, added by gunicorn
  612. "RAW_URI": raw_uri,
  613. "SERVER_NAME": self.server_name,
  614. "SERVER_PORT": str(self.server_port),
  615. "HTTP_HOST": self.host,
  616. "SERVER_PROTOCOL": self.server_protocol,
  617. "wsgi.version": self.wsgi_version,
  618. "wsgi.url_scheme": self.url_scheme,
  619. "wsgi.input": input_stream,
  620. "wsgi.errors": self.errors_stream,
  621. "wsgi.multithread": self.multithread,
  622. "wsgi.multiprocess": self.multiprocess,
  623. "wsgi.run_once": self.run_once,
  624. }
  625. )
  626. headers = self.headers.copy()
  627. # Don't send these as headers, they're part of the environ.
  628. headers.remove("Content-Type")
  629. headers.remove("Content-Length")
  630. if content_type is not None:
  631. result["CONTENT_TYPE"] = content_type
  632. if content_length is not None:
  633. result["CONTENT_LENGTH"] = str(content_length)
  634. combined_headers = defaultdict(list)
  635. for key, value in headers.to_wsgi_list():
  636. combined_headers[f"HTTP_{key.upper().replace('-', '_')}"].append(value)
  637. for key, values in combined_headers.items():
  638. result[key] = ", ".join(values)
  639. if self.environ_overrides:
  640. result.update(self.environ_overrides)
  641. return result
  642. def get_request(self, cls: type[Request] | None = None) -> Request:
  643. """Returns a request with the data. If the request class is not
  644. specified :attr:`request_class` is used.
  645. :param cls: The request wrapper to use.
  646. """
  647. if cls is None:
  648. cls = self.request_class
  649. return cls(self.get_environ())
  650. class ClientRedirectError(Exception):
  651. """If a redirect loop is detected when using follow_redirects=True with
  652. the :cls:`Client`, then this exception is raised.
  653. """
  654. class Client:
  655. """Simulate sending requests to a WSGI application without running a WSGI or HTTP
  656. server.
  657. :param application: The WSGI application to make requests to.
  658. :param response_wrapper: A :class:`.Response` class to wrap response data with.
  659. Defaults to :class:`.TestResponse`. If it's not a subclass of ``TestResponse``,
  660. one will be created.
  661. :param use_cookies: Persist cookies from ``Set-Cookie`` response headers to the
  662. ``Cookie`` header in subsequent requests. Domain and path matching is supported,
  663. but other cookie parameters are ignored.
  664. :param allow_subdomain_redirects: Allow requests to follow redirects to subdomains.
  665. Enable this if the application handles subdomains and redirects between them.
  666. .. versionchanged:: 2.3
  667. Simplify cookie implementation, support domain and path matching.
  668. .. versionchanged:: 2.1
  669. All data is available as properties on the returned response object. The
  670. response cannot be returned as a tuple.
  671. .. versionchanged:: 2.0
  672. ``response_wrapper`` is always a subclass of :class:``TestResponse``.
  673. .. versionchanged:: 0.5
  674. Added the ``use_cookies`` parameter.
  675. """
  676. def __init__(
  677. self,
  678. application: WSGIApplication,
  679. response_wrapper: type[Response] | None = None,
  680. use_cookies: bool = True,
  681. allow_subdomain_redirects: bool = False,
  682. ) -> None:
  683. self.application = application
  684. if response_wrapper in {None, Response}:
  685. response_wrapper = TestResponse
  686. elif response_wrapper is not None and not issubclass(
  687. response_wrapper, TestResponse
  688. ):
  689. response_wrapper = type(
  690. "WrapperTestResponse",
  691. (TestResponse, response_wrapper),
  692. {},
  693. )
  694. self.response_wrapper = t.cast(t.Type["TestResponse"], response_wrapper)
  695. if use_cookies:
  696. self._cookies: dict[tuple[str, str, str], Cookie] | None = {}
  697. else:
  698. self._cookies = None
  699. self.allow_subdomain_redirects = allow_subdomain_redirects
  700. def get_cookie(
  701. self, key: str, domain: str = "localhost", path: str = "/"
  702. ) -> Cookie | None:
  703. """Return a :class:`.Cookie` if it exists. Cookies are uniquely identified by
  704. ``(domain, path, key)``.
  705. :param key: The decoded form of the key for the cookie.
  706. :param domain: The domain the cookie was set for.
  707. :param path: The path the cookie was set for.
  708. .. versionadded:: 2.3
  709. """
  710. if self._cookies is None:
  711. raise TypeError(
  712. "Cookies are disabled. Create a client with 'use_cookies=True'."
  713. )
  714. return self._cookies.get((domain, path, key))
  715. def set_cookie(
  716. self,
  717. key: str,
  718. value: str = "",
  719. *,
  720. domain: str = "localhost",
  721. origin_only: bool = True,
  722. path: str = "/",
  723. **kwargs: t.Any,
  724. ) -> None:
  725. """Set a cookie to be sent in subsequent requests.
  726. This is a convenience to skip making a test request to a route that would set
  727. the cookie. To test the cookie, make a test request to a route that uses the
  728. cookie value.
  729. The client uses ``domain``, ``origin_only``, and ``path`` to determine which
  730. cookies to send with a request. It does not use other cookie parameters that
  731. browsers use, since they're not applicable in tests.
  732. :param key: The key part of the cookie.
  733. :param value: The value part of the cookie.
  734. :param domain: Send this cookie with requests that match this domain. If
  735. ``origin_only`` is true, it must be an exact match, otherwise it may be a
  736. suffix match.
  737. :param origin_only: Whether the domain must be an exact match to the request.
  738. :param path: Send this cookie with requests that match this path either exactly
  739. or as a prefix.
  740. :param kwargs: Passed to :func:`.dump_cookie`.
  741. .. versionchanged:: 3.0
  742. The parameter ``server_name`` is removed. The first parameter is
  743. ``key``. Use the ``domain`` and ``origin_only`` parameters instead.
  744. .. versionchanged:: 2.3
  745. The ``origin_only`` parameter was added.
  746. .. versionchanged:: 2.3
  747. The ``domain`` parameter defaults to ``localhost``.
  748. """
  749. if self._cookies is None:
  750. raise TypeError(
  751. "Cookies are disabled. Create a client with 'use_cookies=True'."
  752. )
  753. cookie = Cookie._from_response_header(
  754. domain, "/", dump_cookie(key, value, domain=domain, path=path, **kwargs)
  755. )
  756. cookie.origin_only = origin_only
  757. if cookie._should_delete:
  758. self._cookies.pop(cookie._storage_key, None)
  759. else:
  760. self._cookies[cookie._storage_key] = cookie
  761. def delete_cookie(
  762. self,
  763. key: str,
  764. *,
  765. domain: str = "localhost",
  766. path: str = "/",
  767. ) -> None:
  768. """Delete a cookie if it exists. Cookies are uniquely identified by
  769. ``(domain, path, key)``.
  770. :param key: The decoded form of the key for the cookie.
  771. :param domain: The domain the cookie was set for.
  772. :param path: The path the cookie was set for.
  773. .. versionchanged:: 3.0
  774. The ``server_name`` parameter is removed. The first parameter is
  775. ``key``. Use the ``domain`` parameter instead.
  776. .. versionchanged:: 3.0
  777. The ``secure``, ``httponly`` and ``samesite`` parameters are removed.
  778. .. versionchanged:: 2.3
  779. The ``domain`` parameter defaults to ``localhost``.
  780. """
  781. if self._cookies is None:
  782. raise TypeError(
  783. "Cookies are disabled. Create a client with 'use_cookies=True'."
  784. )
  785. self._cookies.pop((domain, path, key), None)
  786. def _add_cookies_to_wsgi(self, environ: WSGIEnvironment) -> None:
  787. """If cookies are enabled, set the ``Cookie`` header in the environ to the
  788. cookies that are applicable to the request host and path.
  789. :meta private:
  790. .. versionadded:: 2.3
  791. """
  792. if self._cookies is None:
  793. return
  794. url = urlsplit(get_current_url(environ))
  795. server_name = url.hostname or "localhost"
  796. value = "; ".join(
  797. c._to_request_header()
  798. for c in self._cookies.values()
  799. if c._matches_request(server_name, url.path)
  800. )
  801. if value:
  802. environ["HTTP_COOKIE"] = value
  803. else:
  804. environ.pop("HTTP_COOKIE", None)
  805. def _update_cookies_from_response(
  806. self, server_name: str, path: str, headers: list[str]
  807. ) -> None:
  808. """If cookies are enabled, update the stored cookies from any ``Set-Cookie``
  809. headers in the response.
  810. :meta private:
  811. .. versionadded:: 2.3
  812. """
  813. if self._cookies is None:
  814. return
  815. for header in headers:
  816. cookie = Cookie._from_response_header(server_name, path, header)
  817. if cookie._should_delete:
  818. self._cookies.pop(cookie._storage_key, None)
  819. else:
  820. self._cookies[cookie._storage_key] = cookie
  821. def run_wsgi_app(
  822. self, environ: WSGIEnvironment, buffered: bool = False
  823. ) -> tuple[t.Iterable[bytes], str, Headers]:
  824. """Runs the wrapped WSGI app with the given environment.
  825. :meta private:
  826. """
  827. self._add_cookies_to_wsgi(environ)
  828. rv = run_wsgi_app(self.application, environ, buffered=buffered)
  829. url = urlsplit(get_current_url(environ))
  830. self._update_cookies_from_response(
  831. url.hostname or "localhost", url.path, rv[2].getlist("Set-Cookie")
  832. )
  833. return rv
  834. def resolve_redirect(
  835. self, response: TestResponse, buffered: bool = False
  836. ) -> TestResponse:
  837. """Perform a new request to the location given by the redirect
  838. response to the previous request.
  839. :meta private:
  840. """
  841. scheme, netloc, path, qs, anchor = urlsplit(response.location)
  842. builder = EnvironBuilder.from_environ(
  843. response.request.environ, path=path, query_string=qs
  844. )
  845. to_name_parts = netloc.split(":", 1)[0].split(".")
  846. from_name_parts = builder.server_name.split(".")
  847. if to_name_parts != [""]:
  848. # The new location has a host, use it for the base URL.
  849. builder.url_scheme = scheme
  850. builder.host = netloc
  851. else:
  852. # A local redirect with autocorrect_location_header=False
  853. # doesn't have a host, so use the request's host.
  854. to_name_parts = from_name_parts
  855. # Explain why a redirect to a different server name won't be followed.
  856. if to_name_parts != from_name_parts:
  857. if to_name_parts[-len(from_name_parts) :] == from_name_parts:
  858. if not self.allow_subdomain_redirects:
  859. raise RuntimeError("Following subdomain redirects is not enabled.")
  860. else:
  861. raise RuntimeError("Following external redirects is not supported.")
  862. path_parts = path.split("/")
  863. root_parts = builder.script_root.split("/")
  864. if path_parts[: len(root_parts)] == root_parts:
  865. # Strip the script root from the path.
  866. builder.path = path[len(builder.script_root) :]
  867. else:
  868. # The new location is not under the script root, so use the
  869. # whole path and clear the previous root.
  870. builder.path = path
  871. builder.script_root = ""
  872. # Only 307 and 308 preserve all of the original request.
  873. if response.status_code not in {307, 308}:
  874. # HEAD is preserved, everything else becomes GET.
  875. if builder.method != "HEAD":
  876. builder.method = "GET"
  877. # Clear the body and the headers that describe it.
  878. if builder.input_stream is not None:
  879. builder.input_stream.close()
  880. builder.input_stream = None
  881. builder.content_type = None
  882. builder.content_length = None
  883. builder.headers.pop("Transfer-Encoding", None)
  884. return self.open(builder, buffered=buffered)
  885. def open(
  886. self,
  887. *args: t.Any,
  888. buffered: bool = False,
  889. follow_redirects: bool = False,
  890. **kwargs: t.Any,
  891. ) -> TestResponse:
  892. """Generate an environ dict from the given arguments, make a
  893. request to the application using it, and return the response.
  894. :param args: Passed to :class:`EnvironBuilder` to create the
  895. environ for the request. If a single arg is passed, it can
  896. be an existing :class:`EnvironBuilder` or an environ dict.
  897. :param buffered: Convert the iterator returned by the app into
  898. a list. If the iterator has a ``close()`` method, it is
  899. called automatically.
  900. :param follow_redirects: Make additional requests to follow HTTP
  901. redirects until a non-redirect status is returned.
  902. :attr:`TestResponse.history` lists the intermediate
  903. responses.
  904. .. versionchanged:: 2.1
  905. Removed the ``as_tuple`` parameter.
  906. .. versionchanged:: 2.0
  907. The request input stream is closed when calling
  908. ``response.close()``. Input streams for redirects are
  909. automatically closed.
  910. .. versionchanged:: 0.5
  911. If a dict is provided as file in the dict for the ``data``
  912. parameter the content type has to be called ``content_type``
  913. instead of ``mimetype``. This change was made for
  914. consistency with :class:`werkzeug.FileWrapper`.
  915. .. versionchanged:: 0.5
  916. Added the ``follow_redirects`` parameter.
  917. """
  918. request: Request | None = None
  919. if not kwargs and len(args) == 1:
  920. arg = args[0]
  921. if isinstance(arg, EnvironBuilder):
  922. request = arg.get_request()
  923. elif isinstance(arg, dict):
  924. request = EnvironBuilder.from_environ(arg).get_request()
  925. elif isinstance(arg, Request):
  926. request = arg
  927. if request is None:
  928. builder = EnvironBuilder(*args, **kwargs)
  929. try:
  930. request = builder.get_request()
  931. finally:
  932. builder.close()
  933. response = self.run_wsgi_app(request.environ, buffered=buffered)
  934. response = self.response_wrapper(*response, request=request)
  935. redirects = set()
  936. history: list[TestResponse] = []
  937. if not follow_redirects:
  938. return response
  939. while response.status_code in {
  940. 301,
  941. 302,
  942. 303,
  943. 305,
  944. 307,
  945. 308,
  946. }:
  947. # Exhaust intermediate response bodies to ensure middleware
  948. # that returns an iterator runs any cleanup code.
  949. if not buffered:
  950. response.make_sequence()
  951. response.close()
  952. new_redirect_entry = (response.location, response.status_code)
  953. if new_redirect_entry in redirects:
  954. raise ClientRedirectError(
  955. f"Loop detected: A {response.status_code} redirect"
  956. f" to {response.location} was already made."
  957. )
  958. redirects.add(new_redirect_entry)
  959. response.history = tuple(history)
  960. history.append(response)
  961. response = self.resolve_redirect(response, buffered=buffered)
  962. else:
  963. # This is the final request after redirects.
  964. response.history = tuple(history)
  965. # Close the input stream when closing the response, in case
  966. # the input is an open temporary file.
  967. response.call_on_close(request.input_stream.close)
  968. return response
  969. def get(self, *args: t.Any, **kw: t.Any) -> TestResponse:
  970. """Call :meth:`open` with ``method`` set to ``GET``."""
  971. kw["method"] = "GET"
  972. return self.open(*args, **kw)
  973. def post(self, *args: t.Any, **kw: t.Any) -> TestResponse:
  974. """Call :meth:`open` with ``method`` set to ``POST``."""
  975. kw["method"] = "POST"
  976. return self.open(*args, **kw)
  977. def put(self, *args: t.Any, **kw: t.Any) -> TestResponse:
  978. """Call :meth:`open` with ``method`` set to ``PUT``."""
  979. kw["method"] = "PUT"
  980. return self.open(*args, **kw)
  981. def delete(self, *args: t.Any, **kw: t.Any) -> TestResponse:
  982. """Call :meth:`open` with ``method`` set to ``DELETE``."""
  983. kw["method"] = "DELETE"
  984. return self.open(*args, **kw)
  985. def patch(self, *args: t.Any, **kw: t.Any) -> TestResponse:
  986. """Call :meth:`open` with ``method`` set to ``PATCH``."""
  987. kw["method"] = "PATCH"
  988. return self.open(*args, **kw)
  989. def options(self, *args: t.Any, **kw: t.Any) -> TestResponse:
  990. """Call :meth:`open` with ``method`` set to ``OPTIONS``."""
  991. kw["method"] = "OPTIONS"
  992. return self.open(*args, **kw)
  993. def head(self, *args: t.Any, **kw: t.Any) -> TestResponse:
  994. """Call :meth:`open` with ``method`` set to ``HEAD``."""
  995. kw["method"] = "HEAD"
  996. return self.open(*args, **kw)
  997. def trace(self, *args: t.Any, **kw: t.Any) -> TestResponse:
  998. """Call :meth:`open` with ``method`` set to ``TRACE``."""
  999. kw["method"] = "TRACE"
  1000. return self.open(*args, **kw)
  1001. def __repr__(self) -> str:
  1002. return f"<{type(self).__name__} {self.application!r}>"
  1003. def create_environ(*args: t.Any, **kwargs: t.Any) -> WSGIEnvironment:
  1004. """Create a new WSGI environ dict based on the values passed. The first
  1005. parameter should be the path of the request which defaults to '/'. The
  1006. second one can either be an absolute path (in that case the host is
  1007. localhost:80) or a full path to the request with scheme, netloc port and
  1008. the path to the script.
  1009. This accepts the same arguments as the :class:`EnvironBuilder`
  1010. constructor.
  1011. .. versionchanged:: 0.5
  1012. This function is now a thin wrapper over :class:`EnvironBuilder` which
  1013. was added in 0.5. The `headers`, `environ_base`, `environ_overrides`
  1014. and `charset` parameters were added.
  1015. """
  1016. builder = EnvironBuilder(*args, **kwargs)
  1017. try:
  1018. return builder.get_environ()
  1019. finally:
  1020. builder.close()
  1021. def run_wsgi_app(
  1022. app: WSGIApplication, environ: WSGIEnvironment, buffered: bool = False
  1023. ) -> tuple[t.Iterable[bytes], str, Headers]:
  1024. """Return a tuple in the form (app_iter, status, headers) of the
  1025. application output. This works best if you pass it an application that
  1026. returns an iterator all the time.
  1027. Sometimes applications may use the `write()` callable returned
  1028. by the `start_response` function. This tries to resolve such edge
  1029. cases automatically. But if you don't get the expected output you
  1030. should set `buffered` to `True` which enforces buffering.
  1031. If passed an invalid WSGI application the behavior of this function is
  1032. undefined. Never pass non-conforming WSGI applications to this function.
  1033. :param app: the application to execute.
  1034. :param buffered: set to `True` to enforce buffering.
  1035. :return: tuple in the form ``(app_iter, status, headers)``
  1036. """
  1037. # Copy environ to ensure any mutations by the app (ProxyFix, for
  1038. # example) don't affect subsequent requests (such as redirects).
  1039. environ = _get_environ(environ).copy()
  1040. status: str
  1041. response: tuple[str, list[tuple[str, str]]] | None = None
  1042. buffer: list[bytes] = []
  1043. def start_response(status, headers, exc_info=None): # type: ignore
  1044. nonlocal response
  1045. if exc_info:
  1046. try:
  1047. raise exc_info[1].with_traceback(exc_info[2])
  1048. finally:
  1049. exc_info = None
  1050. response = (status, headers)
  1051. return buffer.append
  1052. app_rv = app(environ, start_response)
  1053. close_func = getattr(app_rv, "close", None)
  1054. app_iter: t.Iterable[bytes] = iter(app_rv)
  1055. # when buffering we emit the close call early and convert the
  1056. # application iterator into a regular list
  1057. if buffered:
  1058. try:
  1059. app_iter = list(app_iter)
  1060. finally:
  1061. if close_func is not None:
  1062. close_func()
  1063. # otherwise we iterate the application iter until we have a response, chain
  1064. # the already received data with the already collected data and wrap it in
  1065. # a new `ClosingIterator` if we need to restore a `close` callable from the
  1066. # original return value.
  1067. else:
  1068. for item in app_iter:
  1069. buffer.append(item)
  1070. if response is not None:
  1071. break
  1072. if buffer:
  1073. app_iter = chain(buffer, app_iter)
  1074. if close_func is not None and app_iter is not app_rv:
  1075. app_iter = ClosingIterator(app_iter, close_func)
  1076. status, headers = response # type: ignore
  1077. return app_iter, status, Headers(headers)
  1078. class TestResponse(Response):
  1079. """:class:`~werkzeug.wrappers.Response` subclass that provides extra
  1080. information about requests made with the test :class:`Client`.
  1081. Test client requests will always return an instance of this class.
  1082. If a custom response class is passed to the client, it is
  1083. subclassed along with this to support test information.
  1084. If the test request included large files, or if the application is
  1085. serving a file, call :meth:`close` to close any open files and
  1086. prevent Python showing a ``ResourceWarning``.
  1087. .. versionchanged:: 2.2
  1088. Set the ``default_mimetype`` to None to prevent a mimetype being
  1089. assumed if missing.
  1090. .. versionchanged:: 2.1
  1091. Response instances cannot be treated as tuples.
  1092. .. versionadded:: 2.0
  1093. Test client methods always return instances of this class.
  1094. """
  1095. default_mimetype = None
  1096. # Don't assume a mimetype, instead use whatever the response provides
  1097. request: Request
  1098. """A request object with the environ used to make the request that
  1099. resulted in this response.
  1100. """
  1101. history: tuple[TestResponse, ...]
  1102. """A list of intermediate responses. Populated when the test request
  1103. is made with ``follow_redirects`` enabled.
  1104. """
  1105. # Tell Pytest to ignore this, it's not a test class.
  1106. __test__ = False
  1107. def __init__(
  1108. self,
  1109. response: t.Iterable[bytes],
  1110. status: str,
  1111. headers: Headers,
  1112. request: Request,
  1113. history: tuple[TestResponse] = (), # type: ignore
  1114. **kwargs: t.Any,
  1115. ) -> None:
  1116. super().__init__(response, status, headers, **kwargs)
  1117. self.request = request
  1118. self.history = history
  1119. self._compat_tuple = response, status, headers
  1120. @cached_property
  1121. def text(self) -> str:
  1122. """The response data as text. A shortcut for
  1123. ``response.get_data(as_text=True)``.
  1124. .. versionadded:: 2.1
  1125. """
  1126. return self.get_data(as_text=True)
  1127. @dataclasses.dataclass
  1128. class Cookie:
  1129. """A cookie key, value, and parameters.
  1130. The class itself is not a public API. Its attributes are documented for inspection
  1131. with :meth:`.Client.get_cookie` only.
  1132. .. versionadded:: 2.3
  1133. """
  1134. key: str
  1135. """The cookie key, encoded as a client would see it."""
  1136. value: str
  1137. """The cookie key, encoded as a client would see it."""
  1138. decoded_key: str
  1139. """The cookie key, decoded as the application would set and see it."""
  1140. decoded_value: str
  1141. """The cookie value, decoded as the application would set and see it."""
  1142. expires: datetime | None
  1143. """The time at which the cookie is no longer valid."""
  1144. max_age: int | None
  1145. """The number of seconds from when the cookie was set at which it is
  1146. no longer valid.
  1147. """
  1148. domain: str
  1149. """The domain that the cookie was set for, or the request domain if not set."""
  1150. origin_only: bool
  1151. """Whether the cookie will be sent for exact domain matches only. This is ``True``
  1152. if the ``Domain`` parameter was not present.
  1153. """
  1154. path: str
  1155. """The path that the cookie was set for."""
  1156. secure: bool | None
  1157. """The ``Secure`` parameter."""
  1158. http_only: bool | None
  1159. """The ``HttpOnly`` parameter."""
  1160. same_site: str | None
  1161. """The ``SameSite`` parameter."""
  1162. def _matches_request(self, server_name: str, path: str) -> bool:
  1163. return (
  1164. server_name == self.domain
  1165. or (
  1166. not self.origin_only
  1167. and server_name.endswith(self.domain)
  1168. and server_name[: -len(self.domain)].endswith(".")
  1169. )
  1170. ) and (
  1171. path == self.path
  1172. or (
  1173. path.startswith(self.path)
  1174. and path[len(self.path) - self.path.endswith("/") :].startswith("/")
  1175. )
  1176. )
  1177. def _to_request_header(self) -> str:
  1178. return f"{self.key}={self.value}"
  1179. @classmethod
  1180. def _from_response_header(cls, server_name: str, path: str, header: str) -> te.Self:
  1181. header, _, parameters_str = header.partition(";")
  1182. key, _, value = header.partition("=")
  1183. decoded_key, decoded_value = next(parse_cookie(header).items())
  1184. params = {}
  1185. for item in parameters_str.split(";"):
  1186. k, sep, v = item.partition("=")
  1187. params[k.strip().lower()] = v.strip() if sep else None
  1188. return cls(
  1189. key=key.strip(),
  1190. value=value.strip(),
  1191. decoded_key=decoded_key,
  1192. decoded_value=decoded_value,
  1193. expires=parse_date(params.get("expires")),
  1194. max_age=int(params["max-age"] or 0) if "max-age" in params else None,
  1195. domain=params.get("domain") or server_name,
  1196. origin_only="domain" not in params,
  1197. path=params.get("path") or path.rpartition("/")[0] or "/",
  1198. secure="secure" in params,
  1199. http_only="httponly" in params,
  1200. same_site=params.get("samesite"),
  1201. )
  1202. @property
  1203. def _storage_key(self) -> tuple[str, str, str]:
  1204. return self.domain, self.path, self.decoded_key
  1205. @property
  1206. def _should_delete(self) -> bool:
  1207. return self.max_age == 0 or (
  1208. self.expires is not None and self.expires.timestamp() == 0
  1209. )