test.py 51 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462
  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 not isinstance(response_wrapper, TestResponse):
  687. response_wrapper = type(
  688. "WrapperTestResponse",
  689. (TestResponse, response_wrapper), # type: ignore
  690. {},
  691. )
  692. self.response_wrapper = t.cast(t.Type["TestResponse"], response_wrapper)
  693. if use_cookies:
  694. self._cookies: dict[tuple[str, str, str], Cookie] | None = {}
  695. else:
  696. self._cookies = None
  697. self.allow_subdomain_redirects = allow_subdomain_redirects
  698. def get_cookie(
  699. self, key: str, domain: str = "localhost", path: str = "/"
  700. ) -> Cookie | None:
  701. """Return a :class:`.Cookie` if it exists. Cookies are uniquely identified by
  702. ``(domain, path, key)``.
  703. :param key: The decoded form of the key for the cookie.
  704. :param domain: The domain the cookie was set for.
  705. :param path: The path the cookie was set for.
  706. .. versionadded:: 2.3
  707. """
  708. if self._cookies is None:
  709. raise TypeError(
  710. "Cookies are disabled. Create a client with 'use_cookies=True'."
  711. )
  712. return self._cookies.get((domain, path, key))
  713. def set_cookie(
  714. self,
  715. key: str,
  716. value: str = "",
  717. *,
  718. domain: str = "localhost",
  719. origin_only: bool = True,
  720. path: str = "/",
  721. **kwargs: t.Any,
  722. ) -> None:
  723. """Set a cookie to be sent in subsequent requests.
  724. This is a convenience to skip making a test request to a route that would set
  725. the cookie. To test the cookie, make a test request to a route that uses the
  726. cookie value.
  727. The client uses ``domain``, ``origin_only``, and ``path`` to determine which
  728. cookies to send with a request. It does not use other cookie parameters that
  729. browsers use, since they're not applicable in tests.
  730. :param key: The key part of the cookie.
  731. :param value: The value part of the cookie.
  732. :param domain: Send this cookie with requests that match this domain. If
  733. ``origin_only`` is true, it must be an exact match, otherwise it may be a
  734. suffix match.
  735. :param origin_only: Whether the domain must be an exact match to the request.
  736. :param path: Send this cookie with requests that match this path either exactly
  737. or as a prefix.
  738. :param kwargs: Passed to :func:`.dump_cookie`.
  739. .. versionchanged:: 3.0
  740. The parameter ``server_name`` is removed. The first parameter is
  741. ``key``. Use the ``domain`` and ``origin_only`` parameters instead.
  742. .. versionchanged:: 2.3
  743. The ``origin_only`` parameter was added.
  744. .. versionchanged:: 2.3
  745. The ``domain`` parameter defaults to ``localhost``.
  746. """
  747. if self._cookies is None:
  748. raise TypeError(
  749. "Cookies are disabled. Create a client with 'use_cookies=True'."
  750. )
  751. cookie = Cookie._from_response_header(
  752. domain, "/", dump_cookie(key, value, domain=domain, path=path, **kwargs)
  753. )
  754. cookie.origin_only = origin_only
  755. if cookie._should_delete:
  756. self._cookies.pop(cookie._storage_key, None)
  757. else:
  758. self._cookies[cookie._storage_key] = cookie
  759. def delete_cookie(
  760. self,
  761. key: str,
  762. *,
  763. domain: str = "localhost",
  764. path: str = "/",
  765. ) -> None:
  766. """Delete a cookie if it exists. Cookies are uniquely identified by
  767. ``(domain, path, key)``.
  768. :param key: The decoded form of the key for the cookie.
  769. :param domain: The domain the cookie was set for.
  770. :param path: The path the cookie was set for.
  771. .. versionchanged:: 3.0
  772. The ``server_name`` parameter is removed. The first parameter is
  773. ``key``. Use the ``domain`` parameter instead.
  774. .. versionchanged:: 3.0
  775. The ``secure``, ``httponly`` and ``samesite`` parameters are removed.
  776. .. versionchanged:: 2.3
  777. The ``domain`` parameter defaults to ``localhost``.
  778. """
  779. if self._cookies is None:
  780. raise TypeError(
  781. "Cookies are disabled. Create a client with 'use_cookies=True'."
  782. )
  783. self._cookies.pop((domain, path, key), None)
  784. def _add_cookies_to_wsgi(self, environ: WSGIEnvironment) -> None:
  785. """If cookies are enabled, set the ``Cookie`` header in the environ to the
  786. cookies that are applicable to the request host and path.
  787. :meta private:
  788. .. versionadded:: 2.3
  789. """
  790. if self._cookies is None:
  791. return
  792. url = urlsplit(get_current_url(environ))
  793. server_name = url.hostname or "localhost"
  794. value = "; ".join(
  795. c._to_request_header()
  796. for c in self._cookies.values()
  797. if c._matches_request(server_name, url.path)
  798. )
  799. if value:
  800. environ["HTTP_COOKIE"] = value
  801. else:
  802. environ.pop("HTTP_COOKIE", None)
  803. def _update_cookies_from_response(
  804. self, server_name: str, path: str, headers: list[str]
  805. ) -> None:
  806. """If cookies are enabled, update the stored cookies from any ``Set-Cookie``
  807. headers in the response.
  808. :meta private:
  809. .. versionadded:: 2.3
  810. """
  811. if self._cookies is None:
  812. return
  813. for header in headers:
  814. cookie = Cookie._from_response_header(server_name, path, header)
  815. if cookie._should_delete:
  816. self._cookies.pop(cookie._storage_key, None)
  817. else:
  818. self._cookies[cookie._storage_key] = cookie
  819. def run_wsgi_app(
  820. self, environ: WSGIEnvironment, buffered: bool = False
  821. ) -> tuple[t.Iterable[bytes], str, Headers]:
  822. """Runs the wrapped WSGI app with the given environment.
  823. :meta private:
  824. """
  825. self._add_cookies_to_wsgi(environ)
  826. rv = run_wsgi_app(self.application, environ, buffered=buffered)
  827. url = urlsplit(get_current_url(environ))
  828. self._update_cookies_from_response(
  829. url.hostname or "localhost", url.path, rv[2].getlist("Set-Cookie")
  830. )
  831. return rv
  832. def resolve_redirect(
  833. self, response: TestResponse, buffered: bool = False
  834. ) -> TestResponse:
  835. """Perform a new request to the location given by the redirect
  836. response to the previous request.
  837. :meta private:
  838. """
  839. scheme, netloc, path, qs, anchor = urlsplit(response.location)
  840. builder = EnvironBuilder.from_environ(
  841. response.request.environ, path=path, query_string=qs
  842. )
  843. to_name_parts = netloc.split(":", 1)[0].split(".")
  844. from_name_parts = builder.server_name.split(".")
  845. if to_name_parts != [""]:
  846. # The new location has a host, use it for the base URL.
  847. builder.url_scheme = scheme
  848. builder.host = netloc
  849. else:
  850. # A local redirect with autocorrect_location_header=False
  851. # doesn't have a host, so use the request's host.
  852. to_name_parts = from_name_parts
  853. # Explain why a redirect to a different server name won't be followed.
  854. if to_name_parts != from_name_parts:
  855. if to_name_parts[-len(from_name_parts) :] == from_name_parts:
  856. if not self.allow_subdomain_redirects:
  857. raise RuntimeError("Following subdomain redirects is not enabled.")
  858. else:
  859. raise RuntimeError("Following external redirects is not supported.")
  860. path_parts = path.split("/")
  861. root_parts = builder.script_root.split("/")
  862. if path_parts[: len(root_parts)] == root_parts:
  863. # Strip the script root from the path.
  864. builder.path = path[len(builder.script_root) :]
  865. else:
  866. # The new location is not under the script root, so use the
  867. # whole path and clear the previous root.
  868. builder.path = path
  869. builder.script_root = ""
  870. # Only 307 and 308 preserve all of the original request.
  871. if response.status_code not in {307, 308}:
  872. # HEAD is preserved, everything else becomes GET.
  873. if builder.method != "HEAD":
  874. builder.method = "GET"
  875. # Clear the body and the headers that describe it.
  876. if builder.input_stream is not None:
  877. builder.input_stream.close()
  878. builder.input_stream = None
  879. builder.content_type = None
  880. builder.content_length = None
  881. builder.headers.pop("Transfer-Encoding", None)
  882. return self.open(builder, buffered=buffered)
  883. def open(
  884. self,
  885. *args: t.Any,
  886. buffered: bool = False,
  887. follow_redirects: bool = False,
  888. **kwargs: t.Any,
  889. ) -> TestResponse:
  890. """Generate an environ dict from the given arguments, make a
  891. request to the application using it, and return the response.
  892. :param args: Passed to :class:`EnvironBuilder` to create the
  893. environ for the request. If a single arg is passed, it can
  894. be an existing :class:`EnvironBuilder` or an environ dict.
  895. :param buffered: Convert the iterator returned by the app into
  896. a list. If the iterator has a ``close()`` method, it is
  897. called automatically.
  898. :param follow_redirects: Make additional requests to follow HTTP
  899. redirects until a non-redirect status is returned.
  900. :attr:`TestResponse.history` lists the intermediate
  901. responses.
  902. .. versionchanged:: 2.1
  903. Removed the ``as_tuple`` parameter.
  904. .. versionchanged:: 2.0
  905. The request input stream is closed when calling
  906. ``response.close()``. Input streams for redirects are
  907. automatically closed.
  908. .. versionchanged:: 0.5
  909. If a dict is provided as file in the dict for the ``data``
  910. parameter the content type has to be called ``content_type``
  911. instead of ``mimetype``. This change was made for
  912. consistency with :class:`werkzeug.FileWrapper`.
  913. .. versionchanged:: 0.5
  914. Added the ``follow_redirects`` parameter.
  915. """
  916. request: Request | None = None
  917. if not kwargs and len(args) == 1:
  918. arg = args[0]
  919. if isinstance(arg, EnvironBuilder):
  920. request = arg.get_request()
  921. elif isinstance(arg, dict):
  922. request = EnvironBuilder.from_environ(arg).get_request()
  923. elif isinstance(arg, Request):
  924. request = arg
  925. if request is None:
  926. builder = EnvironBuilder(*args, **kwargs)
  927. try:
  928. request = builder.get_request()
  929. finally:
  930. builder.close()
  931. response = self.run_wsgi_app(request.environ, buffered=buffered)
  932. response = self.response_wrapper(*response, request=request)
  933. redirects = set()
  934. history: list[TestResponse] = []
  935. if not follow_redirects:
  936. return response
  937. while response.status_code in {
  938. 301,
  939. 302,
  940. 303,
  941. 305,
  942. 307,
  943. 308,
  944. }:
  945. # Exhaust intermediate response bodies to ensure middleware
  946. # that returns an iterator runs any cleanup code.
  947. if not buffered:
  948. response.make_sequence()
  949. response.close()
  950. new_redirect_entry = (response.location, response.status_code)
  951. if new_redirect_entry in redirects:
  952. raise ClientRedirectError(
  953. f"Loop detected: A {response.status_code} redirect"
  954. f" to {response.location} was already made."
  955. )
  956. redirects.add(new_redirect_entry)
  957. response.history = tuple(history)
  958. history.append(response)
  959. response = self.resolve_redirect(response, buffered=buffered)
  960. else:
  961. # This is the final request after redirects.
  962. response.history = tuple(history)
  963. # Close the input stream when closing the response, in case
  964. # the input is an open temporary file.
  965. response.call_on_close(request.input_stream.close)
  966. return response
  967. def get(self, *args: t.Any, **kw: t.Any) -> TestResponse:
  968. """Call :meth:`open` with ``method`` set to ``GET``."""
  969. kw["method"] = "GET"
  970. return self.open(*args, **kw)
  971. def post(self, *args: t.Any, **kw: t.Any) -> TestResponse:
  972. """Call :meth:`open` with ``method`` set to ``POST``."""
  973. kw["method"] = "POST"
  974. return self.open(*args, **kw)
  975. def put(self, *args: t.Any, **kw: t.Any) -> TestResponse:
  976. """Call :meth:`open` with ``method`` set to ``PUT``."""
  977. kw["method"] = "PUT"
  978. return self.open(*args, **kw)
  979. def delete(self, *args: t.Any, **kw: t.Any) -> TestResponse:
  980. """Call :meth:`open` with ``method`` set to ``DELETE``."""
  981. kw["method"] = "DELETE"
  982. return self.open(*args, **kw)
  983. def patch(self, *args: t.Any, **kw: t.Any) -> TestResponse:
  984. """Call :meth:`open` with ``method`` set to ``PATCH``."""
  985. kw["method"] = "PATCH"
  986. return self.open(*args, **kw)
  987. def options(self, *args: t.Any, **kw: t.Any) -> TestResponse:
  988. """Call :meth:`open` with ``method`` set to ``OPTIONS``."""
  989. kw["method"] = "OPTIONS"
  990. return self.open(*args, **kw)
  991. def head(self, *args: t.Any, **kw: t.Any) -> TestResponse:
  992. """Call :meth:`open` with ``method`` set to ``HEAD``."""
  993. kw["method"] = "HEAD"
  994. return self.open(*args, **kw)
  995. def trace(self, *args: t.Any, **kw: t.Any) -> TestResponse:
  996. """Call :meth:`open` with ``method`` set to ``TRACE``."""
  997. kw["method"] = "TRACE"
  998. return self.open(*args, **kw)
  999. def __repr__(self) -> str:
  1000. return f"<{type(self).__name__} {self.application!r}>"
  1001. def create_environ(*args: t.Any, **kwargs: t.Any) -> WSGIEnvironment:
  1002. """Create a new WSGI environ dict based on the values passed. The first
  1003. parameter should be the path of the request which defaults to '/'. The
  1004. second one can either be an absolute path (in that case the host is
  1005. localhost:80) or a full path to the request with scheme, netloc port and
  1006. the path to the script.
  1007. This accepts the same arguments as the :class:`EnvironBuilder`
  1008. constructor.
  1009. .. versionchanged:: 0.5
  1010. This function is now a thin wrapper over :class:`EnvironBuilder` which
  1011. was added in 0.5. The `headers`, `environ_base`, `environ_overrides`
  1012. and `charset` parameters were added.
  1013. """
  1014. builder = EnvironBuilder(*args, **kwargs)
  1015. try:
  1016. return builder.get_environ()
  1017. finally:
  1018. builder.close()
  1019. def run_wsgi_app(
  1020. app: WSGIApplication, environ: WSGIEnvironment, buffered: bool = False
  1021. ) -> tuple[t.Iterable[bytes], str, Headers]:
  1022. """Return a tuple in the form (app_iter, status, headers) of the
  1023. application output. This works best if you pass it an application that
  1024. returns an iterator all the time.
  1025. Sometimes applications may use the `write()` callable returned
  1026. by the `start_response` function. This tries to resolve such edge
  1027. cases automatically. But if you don't get the expected output you
  1028. should set `buffered` to `True` which enforces buffering.
  1029. If passed an invalid WSGI application the behavior of this function is
  1030. undefined. Never pass non-conforming WSGI applications to this function.
  1031. :param app: the application to execute.
  1032. :param buffered: set to `True` to enforce buffering.
  1033. :return: tuple in the form ``(app_iter, status, headers)``
  1034. """
  1035. # Copy environ to ensure any mutations by the app (ProxyFix, for
  1036. # example) don't affect subsequent requests (such as redirects).
  1037. environ = _get_environ(environ).copy()
  1038. status: str
  1039. response: tuple[str, list[tuple[str, str]]] | None = None
  1040. buffer: list[bytes] = []
  1041. def start_response(status, headers, exc_info=None): # type: ignore
  1042. nonlocal response
  1043. if exc_info:
  1044. try:
  1045. raise exc_info[1].with_traceback(exc_info[2])
  1046. finally:
  1047. exc_info = None
  1048. response = (status, headers)
  1049. return buffer.append
  1050. app_rv = app(environ, start_response)
  1051. close_func = getattr(app_rv, "close", None)
  1052. app_iter: t.Iterable[bytes] = iter(app_rv)
  1053. # when buffering we emit the close call early and convert the
  1054. # application iterator into a regular list
  1055. if buffered:
  1056. try:
  1057. app_iter = list(app_iter)
  1058. finally:
  1059. if close_func is not None:
  1060. close_func()
  1061. # otherwise we iterate the application iter until we have a response, chain
  1062. # the already received data with the already collected data and wrap it in
  1063. # a new `ClosingIterator` if we need to restore a `close` callable from the
  1064. # original return value.
  1065. else:
  1066. for item in app_iter:
  1067. buffer.append(item)
  1068. if response is not None:
  1069. break
  1070. if buffer:
  1071. app_iter = chain(buffer, app_iter)
  1072. if close_func is not None and app_iter is not app_rv:
  1073. app_iter = ClosingIterator(app_iter, close_func)
  1074. status, headers = response # type: ignore
  1075. return app_iter, status, Headers(headers)
  1076. class TestResponse(Response):
  1077. """:class:`~werkzeug.wrappers.Response` subclass that provides extra
  1078. information about requests made with the test :class:`Client`.
  1079. Test client requests will always return an instance of this class.
  1080. If a custom response class is passed to the client, it is
  1081. subclassed along with this to support test information.
  1082. If the test request included large files, or if the application is
  1083. serving a file, call :meth:`close` to close any open files and
  1084. prevent Python showing a ``ResourceWarning``.
  1085. .. versionchanged:: 2.2
  1086. Set the ``default_mimetype`` to None to prevent a mimetype being
  1087. assumed if missing.
  1088. .. versionchanged:: 2.1
  1089. Response instances cannot be treated as tuples.
  1090. .. versionadded:: 2.0
  1091. Test client methods always return instances of this class.
  1092. """
  1093. default_mimetype = None
  1094. # Don't assume a mimetype, instead use whatever the response provides
  1095. request: Request
  1096. """A request object with the environ used to make the request that
  1097. resulted in this response.
  1098. """
  1099. history: tuple[TestResponse, ...]
  1100. """A list of intermediate responses. Populated when the test request
  1101. is made with ``follow_redirects`` enabled.
  1102. """
  1103. # Tell Pytest to ignore this, it's not a test class.
  1104. __test__ = False
  1105. def __init__(
  1106. self,
  1107. response: t.Iterable[bytes],
  1108. status: str,
  1109. headers: Headers,
  1110. request: Request,
  1111. history: tuple[TestResponse] = (), # type: ignore
  1112. **kwargs: t.Any,
  1113. ) -> None:
  1114. super().__init__(response, status, headers, **kwargs)
  1115. self.request = request
  1116. self.history = history
  1117. self._compat_tuple = response, status, headers
  1118. @cached_property
  1119. def text(self) -> str:
  1120. """The response data as text. A shortcut for
  1121. ``response.get_data(as_text=True)``.
  1122. .. versionadded:: 2.1
  1123. """
  1124. return self.get_data(as_text=True)
  1125. @dataclasses.dataclass
  1126. class Cookie:
  1127. """A cookie key, value, and parameters.
  1128. The class itself is not a public API. Its attributes are documented for inspection
  1129. with :meth:`.Client.get_cookie` only.
  1130. .. versionadded:: 2.3
  1131. """
  1132. key: str
  1133. """The cookie key, encoded as a client would see it."""
  1134. value: str
  1135. """The cookie key, encoded as a client would see it."""
  1136. decoded_key: str
  1137. """The cookie key, decoded as the application would set and see it."""
  1138. decoded_value: str
  1139. """The cookie value, decoded as the application would set and see it."""
  1140. expires: datetime | None
  1141. """The time at which the cookie is no longer valid."""
  1142. max_age: int | None
  1143. """The number of seconds from when the cookie was set at which it is
  1144. no longer valid.
  1145. """
  1146. domain: str
  1147. """The domain that the cookie was set for, or the request domain if not set."""
  1148. origin_only: bool
  1149. """Whether the cookie will be sent for exact domain matches only. This is ``True``
  1150. if the ``Domain`` parameter was not present.
  1151. """
  1152. path: str
  1153. """The path that the cookie was set for."""
  1154. secure: bool | None
  1155. """The ``Secure`` parameter."""
  1156. http_only: bool | None
  1157. """The ``HttpOnly`` parameter."""
  1158. same_site: str | None
  1159. """The ``SameSite`` parameter."""
  1160. def _matches_request(self, server_name: str, path: str) -> bool:
  1161. return (
  1162. server_name == self.domain
  1163. or (
  1164. not self.origin_only
  1165. and server_name.endswith(self.domain)
  1166. and server_name[: -len(self.domain)].endswith(".")
  1167. )
  1168. ) and (
  1169. path == self.path
  1170. or (
  1171. path.startswith(self.path)
  1172. and path[len(self.path) - self.path.endswith("/") :].startswith("/")
  1173. )
  1174. )
  1175. def _to_request_header(self) -> str:
  1176. return f"{self.key}={self.value}"
  1177. @classmethod
  1178. def _from_response_header(cls, server_name: str, path: str, header: str) -> te.Self:
  1179. header, _, parameters_str = header.partition(";")
  1180. key, _, value = header.partition("=")
  1181. decoded_key, decoded_value = next(parse_cookie(header).items())
  1182. params = {}
  1183. for item in parameters_str.split(";"):
  1184. k, sep, v = item.partition("=")
  1185. params[k.strip().lower()] = v.strip() if sep else None
  1186. return cls(
  1187. key=key.strip(),
  1188. value=value.strip(),
  1189. decoded_key=decoded_key,
  1190. decoded_value=decoded_value,
  1191. expires=parse_date(params.get("expires")),
  1192. max_age=int(params["max-age"] or 0) if "max-age" in params else None,
  1193. domain=params.get("domain") or server_name,
  1194. origin_only="domain" not in params,
  1195. path=params.get("path") or path.rpartition("/")[0] or "/",
  1196. secure="secure" in params,
  1197. http_only="httponly" in params,
  1198. same_site=params.get("samesite"),
  1199. )
  1200. @property
  1201. def _storage_key(self) -> tuple[str, str, str]:
  1202. return self.domain, self.path, self.decoded_key
  1203. @property
  1204. def _should_delete(self) -> bool:
  1205. return self.max_age == 0 or (
  1206. self.expires is not None and self.expires.timestamp() == 0
  1207. )