Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/werkzeug/test.py: 29%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

617 statements  

1from __future__ import annotations 

2 

3import dataclasses 

4import mimetypes 

5import sys 

6import typing as t 

7from collections import defaultdict 

8from datetime import datetime 

9from io import BytesIO 

10from itertools import chain 

11from random import random 

12from tempfile import TemporaryFile 

13from time import time 

14from urllib.parse import unquote 

15from urllib.parse import urlsplit 

16from urllib.parse import urlunsplit 

17 

18from ._internal import _get_environ 

19from ._internal import _wsgi_decoding_dance 

20from ._internal import _wsgi_encoding_dance 

21from .datastructures import Authorization 

22from .datastructures import CallbackDict 

23from .datastructures import CombinedMultiDict 

24from .datastructures import EnvironHeaders 

25from .datastructures import FileMultiDict 

26from .datastructures import Headers 

27from .datastructures import MultiDict 

28from .http import dump_cookie 

29from .http import dump_options_header 

30from .http import parse_cookie 

31from .http import parse_date 

32from .http import parse_options_header 

33from .sansio.multipart import Data 

34from .sansio.multipart import Epilogue 

35from .sansio.multipart import Field 

36from .sansio.multipart import File 

37from .sansio.multipart import MultipartEncoder 

38from .sansio.multipart import Preamble 

39from .urls import _urlencode 

40from .urls import iri_to_uri 

41from .utils import cached_property 

42from .utils import get_content_type 

43from .wrappers.request import Request 

44from .wrappers.response import Response 

45from .wsgi import ClosingIterator 

46from .wsgi import get_current_url 

47 

48if t.TYPE_CHECKING: 

49 import typing_extensions as te 

50 from _typeshed.wsgi import WSGIApplication 

51 from _typeshed.wsgi import WSGIEnvironment 

52 

53 

54def stream_encode_multipart( 

55 data: t.Mapping[str, t.Any], 

56 use_tempfile: bool = True, 

57 threshold: int = 1024 * 500, 

58 boundary: str | None = None, 

59) -> tuple[t.IO[bytes], int, str]: 

60 """Encode a dict of values (either strings or file descriptors or 

61 :class:`FileStorage` objects.) into a multipart encoded string stored 

62 in a file descriptor. 

63 

64 This is not secure and must only be used during testing, never in applications. 

65 

66 .. versionchanged:: 3.0 

67 The ``charset`` parameter was removed. 

68 """ 

69 if boundary is None: 

70 boundary = f"---------------WerkzeugFormPart_{time()}{random()}" 

71 

72 stream: t.IO[bytes] = BytesIO() 

73 total_length = 0 

74 on_disk = False 

75 write_binary: t.Callable[[bytes], int] 

76 

77 if use_tempfile: 

78 

79 def write_binary(s: bytes) -> int: 

80 nonlocal stream, total_length, on_disk 

81 

82 if on_disk: 

83 return stream.write(s) 

84 else: 

85 length = len(s) 

86 

87 if length + total_length <= threshold: 

88 stream.write(s) 

89 else: 

90 new_stream = t.cast(t.IO[bytes], TemporaryFile("wb+")) 

91 new_stream.write(stream.getvalue()) # type: ignore 

92 new_stream.write(s) 

93 stream = new_stream 

94 on_disk = True 

95 

96 total_length += length 

97 return length 

98 

99 else: 

100 write_binary = stream.write 

101 

102 encoder = MultipartEncoder(boundary.encode()) 

103 write_binary(encoder.send_event(Preamble(data=b""))) 

104 for key, value in _iter_data(data): 

105 reader = getattr(value, "read", None) 

106 if reader is not None: 

107 filename = getattr(value, "filename", getattr(value, "name", None)) 

108 content_type = getattr(value, "content_type", None) 

109 if content_type is None: 

110 content_type = ( 

111 filename 

112 and mimetypes.guess_type(filename)[0] 

113 or "application/octet-stream" 

114 ) 

115 headers = value.headers 

116 headers.update([("Content-Type", content_type)]) 

117 if filename is None: 

118 write_binary(encoder.send_event(Field(name=key, headers=headers))) 

119 else: 

120 write_binary( 

121 encoder.send_event( 

122 File(name=key, filename=filename, headers=headers) 

123 ) 

124 ) 

125 while True: 

126 chunk = reader(16384) 

127 

128 if not chunk: 

129 write_binary(encoder.send_event(Data(data=chunk, more_data=False))) 

130 break 

131 

132 write_binary(encoder.send_event(Data(data=chunk, more_data=True))) 

133 else: 

134 if not isinstance(value, str): 

135 value = str(value) 

136 write_binary(encoder.send_event(Field(name=key, headers=Headers()))) 

137 write_binary(encoder.send_event(Data(data=value.encode(), more_data=False))) 

138 

139 write_binary(encoder.send_event(Epilogue(data=b""))) 

140 

141 length = stream.tell() 

142 stream.seek(0) 

143 return stream, length, boundary 

144 

145 

146def encode_multipart( 

147 values: t.Mapping[str, t.Any], boundary: str | None = None 

148) -> tuple[str, bytes]: 

149 """Like `stream_encode_multipart` but returns a tuple in the form 

150 (``boundary``, ``data``) where data is bytes. 

151 

152 This is not secure and must only be used during testing, never in applications. 

153 

154 .. versionchanged:: 3.0 

155 The ``charset`` parameter was removed. 

156 """ 

157 stream, length, boundary = stream_encode_multipart( 

158 values, use_tempfile=False, boundary=boundary 

159 ) 

160 return boundary, stream.read() 

161 

162 

163def _iter_data(data: t.Mapping[str, t.Any]) -> t.Iterator[tuple[str, t.Any]]: 

164 """Iterate over a mapping that might have a list of values, yielding 

165 all key, value pairs. Almost like iter_multi_items but only allows 

166 lists, not tuples, of values so tuples can be used for files. 

167 """ 

168 if isinstance(data, MultiDict): 

169 yield from data.items(multi=True) 

170 else: 

171 for key, value in data.items(): 

172 if isinstance(value, list): 

173 for v in value: 

174 yield key, v 

175 else: 

176 yield key, value 

177 

178 

179_TAnyMultiDict = t.TypeVar("_TAnyMultiDict", bound="MultiDict[t.Any, t.Any]") 

180 

181 

182class EnvironBuilder: 

183 """This class can be used to conveniently create a WSGI environment 

184 for testing purposes. It can be used to quickly create WSGI environments 

185 or request objects from arbitrary data. 

186 

187 The signature of this class is also used in some other places as of 

188 Werkzeug 0.5 (:func:`create_environ`, :meth:`Response.from_values`, 

189 :meth:`Client.open`). Because of this most of the functionality is 

190 available through the constructor alone. 

191 

192 Files and regular form data can be manipulated independently of each 

193 other with the :attr:`form` and :attr:`files` attributes, but are 

194 passed with the same argument to the constructor: `data`. 

195 

196 `data` can be any of these values: 

197 

198 - a `str` or `bytes` object: The object is converted into an 

199 :attr:`input_stream`, the :attr:`content_length` is set and you have to 

200 provide a :attr:`content_type`. 

201 - a `dict` or :class:`MultiDict`: The keys have to be strings. The values 

202 have to be either any of the following objects, or a list of any of the 

203 following objects: 

204 

205 - a :class:`file`-like object: These are converted into 

206 :class:`FileStorage` objects automatically. 

207 - a `tuple`: The :meth:`~FileMultiDict.add_file` method is called 

208 with the key and the unpacked `tuple` items as positional 

209 arguments. 

210 - a `str`: The string is set as form data for the associated key. 

211 - a file-like object: The object content is loaded in memory and then 

212 handled like a regular `str` or a `bytes`. 

213 

214 :param path: the path of the request. In the WSGI environment this will 

215 end up as `PATH_INFO`. If the `query_string` is not defined 

216 and there is a question mark in the `path` everything after 

217 it is used as query string. 

218 :param base_url: the base URL is a URL that is used to extract the WSGI 

219 URL scheme, host (server name + server port) and the 

220 script root (`SCRIPT_NAME`). 

221 :param query_string: an optional string or dict with URL parameters. 

222 :param method: the HTTP method to use, defaults to `GET`. 

223 :param input_stream: an optional input stream. Do not specify this and 

224 `data`. As soon as an input stream is set you can't 

225 modify :attr:`args` and :attr:`files` unless you 

226 set the :attr:`input_stream` to `None` again. 

227 :param content_type: The content type for the request. As of 0.5 you 

228 don't have to provide this when specifying files 

229 and form data via `data`. 

230 :param content_length: The content length for the request. You don't 

231 have to specify this when providing data via 

232 `data`. 

233 :param errors_stream: an optional error stream that is used for 

234 `wsgi.errors`. Defaults to :data:`stderr`. 

235 :param multithread: controls `wsgi.multithread`. Defaults to `False`. 

236 :param multiprocess: controls `wsgi.multiprocess`. Defaults to `False`. 

237 :param run_once: controls `wsgi.run_once`. Defaults to `False`. 

238 :param headers: an optional list or :class:`Headers` object of headers. 

239 :param data: a string or dict of form data or a file-object. 

240 See explanation above. 

241 :param json: An object to be serialized and assigned to ``data``. 

242 Defaults the content type to ``"application/json"``. 

243 Serialized with the function assigned to :attr:`json_dumps`. 

244 :param environ_base: an optional dict of environment defaults. 

245 :param environ_overrides: an optional dict of environment overrides. 

246 :param auth: An authorization object to use for the 

247 ``Authorization`` header value. A ``(username, password)`` tuple 

248 is a shortcut for ``Basic`` authorization. 

249 

250 .. versionchanged:: 3.0 

251 The ``charset`` parameter was removed. 

252 

253 .. versionchanged:: 2.1 

254 ``CONTENT_TYPE`` and ``CONTENT_LENGTH`` are not duplicated as 

255 header keys in the environ. 

256 

257 .. versionchanged:: 2.0 

258 ``REQUEST_URI`` and ``RAW_URI`` is the full raw URI including 

259 the query string, not only the path. 

260 

261 .. versionchanged:: 2.0 

262 The default :attr:`request_class` is ``Request`` instead of 

263 ``BaseRequest``. 

264 

265 .. versionadded:: 2.0 

266 Added the ``auth`` parameter. 

267 

268 .. versionadded:: 0.15 

269 The ``json`` param and :meth:`json_dumps` method. 

270 

271 .. versionadded:: 0.15 

272 The environ has keys ``REQUEST_URI`` and ``RAW_URI`` containing 

273 the path before percent-decoding. This is not part of the WSGI 

274 PEP, but many WSGI servers include it. 

275 

276 .. versionchanged:: 0.6 

277 ``path`` and ``base_url`` can now be unicode strings that are 

278 encoded with :func:`iri_to_uri`. 

279 """ 

280 

281 #: the server protocol to use. defaults to HTTP/1.1 

282 server_protocol = "HTTP/1.1" 

283 

284 #: the wsgi version to use. defaults to (1, 0) 

285 wsgi_version = (1, 0) 

286 

287 #: The default request class used by :meth:`get_request`. 

288 request_class = Request 

289 

290 import json 

291 

292 #: The serialization function used when ``json`` is passed. 

293 json_dumps = staticmethod(json.dumps) 

294 del json 

295 

296 _args: MultiDict[str, str] | None 

297 _query_string: str | None 

298 _input_stream: t.IO[bytes] | None 

299 _form: MultiDict[str, str] | None 

300 _files: FileMultiDict | None 

301 

302 def __init__( 

303 self, 

304 path: str = "/", 

305 base_url: str | None = None, 

306 query_string: t.Mapping[str, str] | str | None = None, 

307 method: str = "GET", 

308 input_stream: t.IO[bytes] | None = None, 

309 content_type: str | None = None, 

310 content_length: int | None = None, 

311 errors_stream: t.IO[str] | None = None, 

312 multithread: bool = False, 

313 multiprocess: bool = False, 

314 run_once: bool = False, 

315 headers: Headers | t.Iterable[tuple[str, str]] | None = None, 

316 data: None | (t.IO[bytes] | str | bytes | t.Mapping[str, t.Any]) = None, 

317 environ_base: t.Mapping[str, t.Any] | None = None, 

318 environ_overrides: t.Mapping[str, t.Any] | None = None, 

319 mimetype: str | None = None, 

320 json: t.Mapping[str, t.Any] | None = None, 

321 auth: Authorization | tuple[str, str] | None = None, 

322 ) -> None: 

323 if query_string is not None and "?" in path: 

324 raise ValueError("Query string is defined in the path and as an argument") 

325 request_uri = urlsplit(path) 

326 if query_string is None and "?" in path: 

327 query_string = request_uri.query 

328 

329 self.path = iri_to_uri(request_uri.path) 

330 self.request_uri = path 

331 if base_url is not None: 

332 base_url = iri_to_uri(base_url) 

333 self.base_url = base_url 

334 if isinstance(query_string, str): 

335 self.query_string = query_string 

336 else: 

337 if query_string is None: 

338 query_string = MultiDict() 

339 elif not isinstance(query_string, MultiDict): 

340 query_string = MultiDict(query_string) 

341 self.args = query_string 

342 self.method = method 

343 if headers is None: 

344 headers = Headers() 

345 elif not isinstance(headers, Headers): 

346 headers = Headers(headers) 

347 self.headers = headers 

348 if content_type is not None: 

349 self.content_type = content_type 

350 if errors_stream is None: 

351 errors_stream = sys.stderr 

352 self.errors_stream = errors_stream 

353 self.multithread = multithread 

354 self.multiprocess = multiprocess 

355 self.run_once = run_once 

356 self.environ_base = environ_base 

357 self.environ_overrides = environ_overrides 

358 self.input_stream = input_stream 

359 self.content_length = content_length 

360 self.closed = False 

361 

362 if auth is not None: 

363 if isinstance(auth, tuple): 

364 auth = Authorization( 

365 "basic", {"username": auth[0], "password": auth[1]} 

366 ) 

367 

368 self.headers.set("Authorization", auth.to_header()) 

369 

370 if json is not None: 

371 if data is not None: 

372 raise TypeError("can't provide both json and data") 

373 

374 data = self.json_dumps(json) 

375 

376 if self.content_type is None: 

377 self.content_type = "application/json" 

378 

379 if data: 

380 if input_stream is not None: 

381 raise TypeError("can't provide input stream and data") 

382 if hasattr(data, "read"): 

383 data = data.read() 

384 if isinstance(data, str): 

385 data = data.encode() 

386 if isinstance(data, bytes): 

387 self.input_stream = BytesIO(data) 

388 if self.content_length is None: 

389 self.content_length = len(data) 

390 else: 

391 for key, value in _iter_data(data): 

392 if isinstance(value, (tuple, dict)) or hasattr(value, "read"): 

393 self._add_file_from_data(key, value) 

394 else: 

395 self.form.setlistdefault(key).append(value) 

396 

397 if mimetype is not None: 

398 self.mimetype = mimetype 

399 

400 @classmethod 

401 def from_environ(cls, environ: WSGIEnvironment, **kwargs: t.Any) -> EnvironBuilder: 

402 """Turn an environ dict back into a builder. Any extra kwargs 

403 override the args extracted from the environ. 

404 

405 .. versionchanged:: 2.0 

406 Path and query values are passed through the WSGI decoding 

407 dance to avoid double encoding. 

408 

409 .. versionadded:: 0.15 

410 """ 

411 headers = Headers(EnvironHeaders(environ)) 

412 out = { 

413 "path": _wsgi_decoding_dance(environ["PATH_INFO"]), 

414 "base_url": cls._make_base_url( 

415 environ["wsgi.url_scheme"], 

416 headers.pop("Host"), 

417 _wsgi_decoding_dance(environ["SCRIPT_NAME"]), 

418 ), 

419 "query_string": _wsgi_decoding_dance(environ["QUERY_STRING"]), 

420 "method": environ["REQUEST_METHOD"], 

421 "input_stream": environ["wsgi.input"], 

422 "content_type": headers.pop("Content-Type", None), 

423 "content_length": headers.pop("Content-Length", None), 

424 "errors_stream": environ["wsgi.errors"], 

425 "multithread": environ["wsgi.multithread"], 

426 "multiprocess": environ["wsgi.multiprocess"], 

427 "run_once": environ["wsgi.run_once"], 

428 "headers": headers, 

429 } 

430 out.update(kwargs) 

431 return cls(**out) 

432 

433 def _add_file_from_data( 

434 self, 

435 key: str, 

436 value: (t.IO[bytes] | tuple[t.IO[bytes], str] | tuple[t.IO[bytes], str, str]), 

437 ) -> None: 

438 """Called in the EnvironBuilder to add files from the data dict.""" 

439 if isinstance(value, tuple): 

440 self.files.add_file(key, *value) 

441 else: 

442 self.files.add_file(key, value) 

443 

444 @staticmethod 

445 def _make_base_url(scheme: str, host: str, script_root: str) -> str: 

446 return urlunsplit((scheme, host, script_root, "", "")).rstrip("/") + "/" 

447 

448 @property 

449 def base_url(self) -> str: 

450 """The base URL is used to extract the URL scheme, host name, 

451 port, and root path. 

452 """ 

453 return self._make_base_url(self.url_scheme, self.host, self.script_root) 

454 

455 @base_url.setter 

456 def base_url(self, value: str | None) -> None: 

457 if value is None: 

458 scheme = "http" 

459 netloc = "localhost" 

460 script_root = "" 

461 else: 

462 scheme, netloc, script_root, qs, anchor = urlsplit(value) 

463 if qs or anchor: 

464 raise ValueError("base url must not contain a query string or fragment") 

465 self.script_root = script_root.rstrip("/") 

466 self.host = netloc 

467 self.url_scheme = scheme 

468 

469 @property 

470 def content_type(self) -> str | None: 

471 """The content type for the request. Reflected from and to 

472 the :attr:`headers`. Do not set if you set :attr:`files` or 

473 :attr:`form` for auto detection. 

474 """ 

475 ct = self.headers.get("Content-Type") 

476 if ct is None and not self._input_stream: 

477 if self._files: 

478 return "multipart/form-data" 

479 if self._form: 

480 return "application/x-www-form-urlencoded" 

481 return None 

482 return ct 

483 

484 @content_type.setter 

485 def content_type(self, value: str | None) -> None: 

486 if value is None: 

487 self.headers.pop("Content-Type", None) 

488 else: 

489 self.headers["Content-Type"] = value 

490 

491 @property 

492 def mimetype(self) -> str | None: 

493 """The mimetype (content type without charset etc.) 

494 

495 .. versionadded:: 0.14 

496 """ 

497 ct = self.content_type 

498 return ct.partition(";")[0].strip() if ct else None 

499 

500 @mimetype.setter 

501 def mimetype(self, value: str) -> None: 

502 self.content_type = get_content_type(value, "utf-8") 

503 

504 @property 

505 def mimetype_params(self) -> t.Mapping[str, str]: 

506 """The mimetype parameters as dict. For example if the 

507 content type is ``text/html; charset=utf-8`` the params would be 

508 ``{'charset': 'utf-8'}``. 

509 

510 .. versionadded:: 0.14 

511 """ 

512 

513 def on_update(d: CallbackDict[str, str]) -> None: 

514 self.headers["Content-Type"] = dump_options_header(self.mimetype, d) 

515 

516 d = parse_options_header(self.headers.get("content-type", ""))[1] 

517 return CallbackDict(d, on_update) 

518 

519 @property 

520 def content_length(self) -> int | None: 

521 """The content length as integer. Reflected from and to the 

522 :attr:`headers`. Do not set if you set :attr:`files` or 

523 :attr:`form` for auto detection. 

524 """ 

525 return self.headers.get("Content-Length", type=int) 

526 

527 @content_length.setter 

528 def content_length(self, value: int | None) -> None: 

529 if value is None: 

530 self.headers.pop("Content-Length", None) 

531 else: 

532 self.headers["Content-Length"] = str(value) 

533 

534 def _get_form(self, name: str, storage: type[_TAnyMultiDict]) -> _TAnyMultiDict: 

535 """Common behavior for getting the :attr:`form` and 

536 :attr:`files` properties. 

537 

538 :param name: Name of the internal cached attribute. 

539 :param storage: Storage class used for the data. 

540 """ 

541 if self.input_stream is not None: 

542 raise AttributeError("an input stream is defined") 

543 

544 rv = getattr(self, name) 

545 

546 if rv is None: 

547 rv = storage() 

548 setattr(self, name, rv) 

549 

550 return rv # type: ignore 

551 

552 def _set_form(self, name: str, value: MultiDict[str, t.Any]) -> None: 

553 """Common behavior for setting the :attr:`form` and 

554 :attr:`files` properties. 

555 

556 :param name: Name of the internal cached attribute. 

557 :param value: Value to assign to the attribute. 

558 """ 

559 self._input_stream = None 

560 setattr(self, name, value) 

561 

562 @property 

563 def form(self) -> MultiDict[str, str]: 

564 """A :class:`MultiDict` of form values.""" 

565 return self._get_form("_form", MultiDict) 

566 

567 @form.setter 

568 def form(self, value: MultiDict[str, str]) -> None: 

569 self._set_form("_form", value) 

570 

571 @property 

572 def files(self) -> FileMultiDict: 

573 """A :class:`FileMultiDict` of uploaded files. Use 

574 :meth:`~FileMultiDict.add_file` to add new files. 

575 """ 

576 return self._get_form("_files", FileMultiDict) 

577 

578 @files.setter 

579 def files(self, value: FileMultiDict) -> None: 

580 self._set_form("_files", value) 

581 

582 @property 

583 def input_stream(self) -> t.IO[bytes] | None: 

584 """An optional input stream. This is mutually exclusive with 

585 setting :attr:`form` and :attr:`files`, setting it will clear 

586 those. Do not provide this if the method is not ``POST`` or 

587 another method that has a body. 

588 """ 

589 return self._input_stream 

590 

591 @input_stream.setter 

592 def input_stream(self, value: t.IO[bytes] | None) -> None: 

593 self._input_stream = value 

594 self._form = None 

595 self._files = None 

596 

597 @property 

598 def query_string(self) -> str: 

599 """The query string. If you set this to a string 

600 :attr:`args` will no longer be available. 

601 """ 

602 if self._query_string is None: 

603 if self._args is not None: 

604 return _urlencode(self._args) 

605 return "" 

606 return self._query_string 

607 

608 @query_string.setter 

609 def query_string(self, value: str | None) -> None: 

610 self._query_string = value 

611 self._args = None 

612 

613 @property 

614 def args(self) -> MultiDict[str, str]: 

615 """The URL arguments as :class:`MultiDict`.""" 

616 if self._query_string is not None: 

617 raise AttributeError("a query string is defined") 

618 if self._args is None: 

619 self._args = MultiDict() 

620 return self._args 

621 

622 @args.setter 

623 def args(self, value: MultiDict[str, str] | None) -> None: 

624 self._query_string = None 

625 self._args = value 

626 

627 @property 

628 def server_name(self) -> str: 

629 """The server name (read-only, use :attr:`host` to set)""" 

630 return self.host.partition(":")[0] 

631 

632 @property 

633 def server_port(self) -> int: 

634 """The server port as integer (read-only, use :attr:`host` to set)""" 

635 _, sep, port = self.host.partition(":") 

636 

637 if sep: 

638 try: 

639 return int(port) 

640 except ValueError: 

641 pass 

642 

643 if self.url_scheme == "https": 

644 return 443 

645 return 80 

646 

647 def __del__(self) -> None: 

648 try: 

649 self.close() 

650 except Exception: 

651 pass 

652 

653 def close(self) -> None: 

654 """Closes all files. If you put real :class:`file` objects into the 

655 :attr:`files` dict you can call this method to automatically close 

656 them all in one go. 

657 """ 

658 if self.closed: 

659 return 

660 try: 

661 files = self.files.values() 

662 except AttributeError: 

663 files = () 

664 for f in files: 

665 try: 

666 f.close() 

667 except Exception: 

668 pass 

669 self.closed = True 

670 

671 def get_environ(self) -> WSGIEnvironment: 

672 """Return the built environ. 

673 

674 .. versionchanged:: 0.15 

675 The content type and length headers are set based on 

676 input stream detection. Previously this only set the WSGI 

677 keys. 

678 """ 

679 input_stream = self.input_stream 

680 content_length = self.content_length 

681 

682 mimetype = self.mimetype 

683 content_type = self.content_type 

684 

685 if input_stream is not None: 

686 start_pos = input_stream.tell() 

687 input_stream.seek(0, 2) 

688 end_pos = input_stream.tell() 

689 input_stream.seek(start_pos) 

690 content_length = end_pos - start_pos 

691 elif mimetype == "multipart/form-data": 

692 input_stream, content_length, boundary = stream_encode_multipart( 

693 CombinedMultiDict([self.form, self.files]) 

694 ) 

695 content_type = f'{mimetype}; boundary="{boundary}"' 

696 elif mimetype == "application/x-www-form-urlencoded": 

697 form_encoded = _urlencode(self.form).encode("ascii") 

698 content_length = len(form_encoded) 

699 input_stream = BytesIO(form_encoded) 

700 else: 

701 input_stream = BytesIO() 

702 

703 result: WSGIEnvironment = {} 

704 if self.environ_base: 

705 result.update(self.environ_base) 

706 

707 def _path_encode(x: str) -> str: 

708 return _wsgi_encoding_dance(unquote(x)) 

709 

710 raw_uri = _wsgi_encoding_dance(self.request_uri) 

711 result.update( 

712 { 

713 "REQUEST_METHOD": self.method, 

714 "SCRIPT_NAME": _path_encode(self.script_root), 

715 "PATH_INFO": _path_encode(self.path), 

716 "QUERY_STRING": _wsgi_encoding_dance(self.query_string), 

717 # Non-standard, added by mod_wsgi, uWSGI 

718 "REQUEST_URI": raw_uri, 

719 # Non-standard, added by gunicorn 

720 "RAW_URI": raw_uri, 

721 "SERVER_NAME": self.server_name, 

722 "SERVER_PORT": str(self.server_port), 

723 "HTTP_HOST": self.host, 

724 "SERVER_PROTOCOL": self.server_protocol, 

725 "wsgi.version": self.wsgi_version, 

726 "wsgi.url_scheme": self.url_scheme, 

727 "wsgi.input": input_stream, 

728 "wsgi.errors": self.errors_stream, 

729 "wsgi.multithread": self.multithread, 

730 "wsgi.multiprocess": self.multiprocess, 

731 "wsgi.run_once": self.run_once, 

732 } 

733 ) 

734 

735 headers = self.headers.copy() 

736 # Don't send these as headers, they're part of the environ. 

737 headers.remove("Content-Type") 

738 headers.remove("Content-Length") 

739 

740 if content_type is not None: 

741 result["CONTENT_TYPE"] = content_type 

742 

743 if content_length is not None: 

744 result["CONTENT_LENGTH"] = str(content_length) 

745 

746 combined_headers = defaultdict(list) 

747 

748 for key, value in headers.to_wsgi_list(): 

749 combined_headers[f"HTTP_{key.upper().replace('-', '_')}"].append(value) 

750 

751 for key, values in combined_headers.items(): 

752 result[key] = ", ".join(values) 

753 

754 if self.environ_overrides: 

755 result.update(self.environ_overrides) 

756 

757 return result 

758 

759 def get_request(self, cls: type[Request] | None = None) -> Request: 

760 """Returns a request with the data. If the request class is not 

761 specified :attr:`request_class` is used. 

762 

763 :param cls: The request wrapper to use. 

764 """ 

765 if cls is None: 

766 cls = self.request_class 

767 

768 return cls(self.get_environ()) 

769 

770 

771class ClientRedirectError(Exception): 

772 """If a redirect loop is detected when using follow_redirects=True with 

773 the :cls:`Client`, then this exception is raised. 

774 """ 

775 

776 

777class Client: 

778 """Simulate sending requests to a WSGI application without running a WSGI or HTTP 

779 server. 

780 

781 :param application: The WSGI application to make requests to. 

782 :param response_wrapper: A :class:`.Response` class to wrap response data with. 

783 Defaults to :class:`.TestResponse`. If it's not a subclass of ``TestResponse``, 

784 one will be created. 

785 :param use_cookies: Persist cookies from ``Set-Cookie`` response headers to the 

786 ``Cookie`` header in subsequent requests. Domain and path matching is supported, 

787 but other cookie parameters are ignored. 

788 :param allow_subdomain_redirects: Allow requests to follow redirects to subdomains. 

789 Enable this if the application handles subdomains and redirects between them. 

790 

791 .. versionchanged:: 2.3 

792 Simplify cookie implementation, support domain and path matching. 

793 

794 .. versionchanged:: 2.1 

795 All data is available as properties on the returned response object. The 

796 response cannot be returned as a tuple. 

797 

798 .. versionchanged:: 2.0 

799 ``response_wrapper`` is always a subclass of :class:``TestResponse``. 

800 

801 .. versionchanged:: 0.5 

802 Added the ``use_cookies`` parameter. 

803 """ 

804 

805 def __init__( 

806 self, 

807 application: WSGIApplication, 

808 response_wrapper: type[Response] | None = None, 

809 use_cookies: bool = True, 

810 allow_subdomain_redirects: bool = False, 

811 ) -> None: 

812 self.application = application 

813 

814 if response_wrapper in {None, Response}: 

815 response_wrapper = TestResponse 

816 elif response_wrapper is not None and not issubclass( 

817 response_wrapper, TestResponse 

818 ): 

819 response_wrapper = type( 

820 "WrapperTestResponse", 

821 (TestResponse, response_wrapper), 

822 {}, 

823 ) 

824 

825 self.response_wrapper = t.cast(type["TestResponse"], response_wrapper) 

826 

827 if use_cookies: 

828 self._cookies: dict[tuple[str, str, str], Cookie] | None = {} 

829 else: 

830 self._cookies = None 

831 

832 self.allow_subdomain_redirects = allow_subdomain_redirects 

833 

834 def get_cookie( 

835 self, key: str, domain: str = "localhost", path: str = "/" 

836 ) -> Cookie | None: 

837 """Return a :class:`.Cookie` if it exists. Cookies are uniquely identified by 

838 ``(domain, path, key)``. 

839 

840 :param key: The decoded form of the key for the cookie. 

841 :param domain: The domain the cookie was set for. 

842 :param path: The path the cookie was set for. 

843 

844 .. versionadded:: 2.3 

845 """ 

846 if self._cookies is None: 

847 raise TypeError( 

848 "Cookies are disabled. Create a client with 'use_cookies=True'." 

849 ) 

850 

851 return self._cookies.get((domain, path, key)) 

852 

853 def set_cookie( 

854 self, 

855 key: str, 

856 value: str = "", 

857 *, 

858 domain: str = "localhost", 

859 origin_only: bool = True, 

860 path: str = "/", 

861 **kwargs: t.Any, 

862 ) -> None: 

863 """Set a cookie to be sent in subsequent requests. 

864 

865 This is a convenience to skip making a test request to a route that would set 

866 the cookie. To test the cookie, make a test request to a route that uses the 

867 cookie value. 

868 

869 The client uses ``domain``, ``origin_only``, and ``path`` to determine which 

870 cookies to send with a request. It does not use other cookie parameters that 

871 browsers use, since they're not applicable in tests. 

872 

873 :param key: The key part of the cookie. 

874 :param value: The value part of the cookie. 

875 :param domain: Send this cookie with requests that match this domain. If 

876 ``origin_only`` is true, it must be an exact match, otherwise it may be a 

877 suffix match. 

878 :param origin_only: Whether the domain must be an exact match to the request. 

879 :param path: Send this cookie with requests that match this path either exactly 

880 or as a prefix. 

881 :param kwargs: Passed to :func:`.dump_cookie`. 

882 

883 .. versionchanged:: 3.0 

884 The parameter ``server_name`` is removed. The first parameter is 

885 ``key``. Use the ``domain`` and ``origin_only`` parameters instead. 

886 

887 .. versionchanged:: 2.3 

888 The ``origin_only`` parameter was added. 

889 

890 .. versionchanged:: 2.3 

891 The ``domain`` parameter defaults to ``localhost``. 

892 """ 

893 if self._cookies is None: 

894 raise TypeError( 

895 "Cookies are disabled. Create a client with 'use_cookies=True'." 

896 ) 

897 

898 cookie = Cookie._from_response_header( 

899 domain, "/", dump_cookie(key, value, domain=domain, path=path, **kwargs) 

900 ) 

901 cookie.origin_only = origin_only 

902 

903 if cookie._should_delete: 

904 self._cookies.pop(cookie._storage_key, None) 

905 else: 

906 self._cookies[cookie._storage_key] = cookie 

907 

908 def delete_cookie( 

909 self, 

910 key: str, 

911 *, 

912 domain: str = "localhost", 

913 path: str = "/", 

914 ) -> None: 

915 """Delete a cookie if it exists. Cookies are uniquely identified by 

916 ``(domain, path, key)``. 

917 

918 :param key: The decoded form of the key for the cookie. 

919 :param domain: The domain the cookie was set for. 

920 :param path: The path the cookie was set for. 

921 

922 .. versionchanged:: 3.0 

923 The ``server_name`` parameter is removed. The first parameter is 

924 ``key``. Use the ``domain`` parameter instead. 

925 

926 .. versionchanged:: 3.0 

927 The ``secure``, ``httponly`` and ``samesite`` parameters are removed. 

928 

929 .. versionchanged:: 2.3 

930 The ``domain`` parameter defaults to ``localhost``. 

931 """ 

932 if self._cookies is None: 

933 raise TypeError( 

934 "Cookies are disabled. Create a client with 'use_cookies=True'." 

935 ) 

936 

937 self._cookies.pop((domain, path, key), None) 

938 

939 def _add_cookies_to_wsgi(self, environ: WSGIEnvironment) -> None: 

940 """If cookies are enabled, set the ``Cookie`` header in the environ to the 

941 cookies that are applicable to the request host and path. 

942 

943 :meta private: 

944 

945 .. versionadded:: 2.3 

946 """ 

947 if self._cookies is None: 

948 return 

949 

950 url = urlsplit(get_current_url(environ)) 

951 server_name = url.hostname or "localhost" 

952 value = "; ".join( 

953 c._to_request_header() 

954 for c in self._cookies.values() 

955 if c._matches_request(server_name, url.path) 

956 ) 

957 

958 if value: 

959 environ["HTTP_COOKIE"] = value 

960 else: 

961 environ.pop("HTTP_COOKIE", None) 

962 

963 def _update_cookies_from_response( 

964 self, server_name: str, path: str, headers: list[str] 

965 ) -> None: 

966 """If cookies are enabled, update the stored cookies from any ``Set-Cookie`` 

967 headers in the response. 

968 

969 :meta private: 

970 

971 .. versionadded:: 2.3 

972 """ 

973 if self._cookies is None: 

974 return 

975 

976 for header in headers: 

977 cookie = Cookie._from_response_header(server_name, path, header) 

978 

979 if cookie._should_delete: 

980 self._cookies.pop(cookie._storage_key, None) 

981 else: 

982 self._cookies[cookie._storage_key] = cookie 

983 

984 def run_wsgi_app( 

985 self, environ: WSGIEnvironment, buffered: bool = False 

986 ) -> tuple[t.Iterable[bytes], str, Headers]: 

987 """Runs the wrapped WSGI app with the given environment. 

988 

989 :meta private: 

990 """ 

991 self._add_cookies_to_wsgi(environ) 

992 rv = run_wsgi_app(self.application, environ, buffered=buffered) 

993 url = urlsplit(get_current_url(environ)) 

994 self._update_cookies_from_response( 

995 url.hostname or "localhost", url.path, rv[2].getlist("Set-Cookie") 

996 ) 

997 return rv 

998 

999 def resolve_redirect( 

1000 self, response: TestResponse, buffered: bool = False 

1001 ) -> TestResponse: 

1002 """Perform a new request to the location given by the redirect 

1003 response to the previous request. 

1004 

1005 :meta private: 

1006 """ 

1007 scheme, netloc, path, qs, anchor = urlsplit(response.location) 

1008 builder = EnvironBuilder.from_environ( 

1009 response.request.environ, path=path, query_string=qs 

1010 ) 

1011 

1012 to_name_parts = netloc.partition(":")[0].split(".") 

1013 from_name_parts = builder.server_name.split(".") 

1014 

1015 if to_name_parts != [""]: 

1016 # The new location has a host, use it for the base URL. 

1017 builder.url_scheme = scheme 

1018 builder.host = netloc 

1019 else: 

1020 # A local redirect with autocorrect_location_header=False 

1021 # doesn't have a host, so use the request's host. 

1022 to_name_parts = from_name_parts 

1023 

1024 # Explain why a redirect to a different server name won't be followed. 

1025 if to_name_parts != from_name_parts: 

1026 if to_name_parts[-len(from_name_parts) :] == from_name_parts: 

1027 if not self.allow_subdomain_redirects: 

1028 raise RuntimeError("Following subdomain redirects is not enabled.") 

1029 else: 

1030 raise RuntimeError("Following external redirects is not supported.") 

1031 

1032 path_parts = path.split("/") 

1033 root_parts = builder.script_root.split("/") 

1034 

1035 if path_parts[: len(root_parts)] == root_parts: 

1036 # Strip the script root from the path. 

1037 builder.path = path[len(builder.script_root) :] 

1038 else: 

1039 # The new location is not under the script root, so use the 

1040 # whole path and clear the previous root. 

1041 builder.path = path 

1042 builder.script_root = "" 

1043 

1044 # Only 307 and 308 preserve all of the original request. 

1045 if response.status_code not in {307, 308}: 

1046 # HEAD is preserved, everything else becomes GET. 

1047 if builder.method != "HEAD": 

1048 builder.method = "GET" 

1049 

1050 # Clear the body and the headers that describe it. 

1051 

1052 if builder.input_stream is not None: 

1053 builder.input_stream.close() 

1054 builder.input_stream = None 

1055 

1056 builder.content_type = None 

1057 builder.content_length = None 

1058 builder.headers.pop("Transfer-Encoding", None) 

1059 

1060 return self.open(builder, buffered=buffered) 

1061 

1062 def open( 

1063 self, 

1064 *args: t.Any, 

1065 buffered: bool = False, 

1066 follow_redirects: bool = False, 

1067 **kwargs: t.Any, 

1068 ) -> TestResponse: 

1069 """Generate an environ dict from the given arguments, make a 

1070 request to the application using it, and return the response. 

1071 

1072 :param args: Passed to :class:`EnvironBuilder` to create the 

1073 environ for the request. If a single arg is passed, it can 

1074 be an existing :class:`EnvironBuilder` or an environ dict. 

1075 :param buffered: Convert the iterator returned by the app into 

1076 a list. If the iterator has a ``close()`` method, it is 

1077 called automatically. 

1078 :param follow_redirects: Make additional requests to follow HTTP 

1079 redirects until a non-redirect status is returned. 

1080 :attr:`TestResponse.history` lists the intermediate 

1081 responses. 

1082 

1083 .. versionchanged:: 2.1 

1084 Removed the ``as_tuple`` parameter. 

1085 

1086 .. versionchanged:: 2.0 

1087 The request input stream is closed when calling 

1088 ``response.close()``. Input streams for redirects are 

1089 automatically closed. 

1090 

1091 .. versionchanged:: 0.5 

1092 If a dict is provided as file in the dict for the ``data`` 

1093 parameter the content type has to be called ``content_type`` 

1094 instead of ``mimetype``. This change was made for 

1095 consistency with :class:`werkzeug.FileWrapper`. 

1096 

1097 .. versionchanged:: 0.5 

1098 Added the ``follow_redirects`` parameter. 

1099 """ 

1100 request: Request | None = None 

1101 

1102 if not kwargs and len(args) == 1: 

1103 arg = args[0] 

1104 

1105 if isinstance(arg, EnvironBuilder): 

1106 request = arg.get_request() 

1107 elif isinstance(arg, dict): 

1108 request = EnvironBuilder.from_environ(arg).get_request() 

1109 elif isinstance(arg, Request): 

1110 request = arg 

1111 

1112 if request is None: 

1113 builder = EnvironBuilder(*args, **kwargs) 

1114 

1115 try: 

1116 request = builder.get_request() 

1117 finally: 

1118 builder.close() 

1119 

1120 response_parts = self.run_wsgi_app(request.environ, buffered=buffered) 

1121 response = self.response_wrapper(*response_parts, request=request) 

1122 

1123 redirects = set() 

1124 history: list[TestResponse] = [] 

1125 

1126 if not follow_redirects: 

1127 return response 

1128 

1129 while response.status_code in { 

1130 301, 

1131 302, 

1132 303, 

1133 305, 

1134 307, 

1135 308, 

1136 }: 

1137 # Exhaust intermediate response bodies to ensure middleware 

1138 # that returns an iterator runs any cleanup code. 

1139 if not buffered: 

1140 response.make_sequence() 

1141 response.close() 

1142 

1143 new_redirect_entry = (response.location, response.status_code) 

1144 

1145 if new_redirect_entry in redirects: 

1146 raise ClientRedirectError( 

1147 f"Loop detected: A {response.status_code} redirect" 

1148 f" to {response.location} was already made." 

1149 ) 

1150 

1151 redirects.add(new_redirect_entry) 

1152 response.history = tuple(history) 

1153 history.append(response) 

1154 response = self.resolve_redirect(response, buffered=buffered) 

1155 else: 

1156 # This is the final request after redirects. 

1157 response.history = tuple(history) 

1158 # Close the input stream when closing the response, in case 

1159 # the input is an open temporary file. 

1160 response.call_on_close(request.input_stream.close) 

1161 return response 

1162 

1163 def get(self, *args: t.Any, **kw: t.Any) -> TestResponse: 

1164 """Call :meth:`open` with ``method`` set to ``GET``.""" 

1165 kw["method"] = "GET" 

1166 return self.open(*args, **kw) 

1167 

1168 def post(self, *args: t.Any, **kw: t.Any) -> TestResponse: 

1169 """Call :meth:`open` with ``method`` set to ``POST``.""" 

1170 kw["method"] = "POST" 

1171 return self.open(*args, **kw) 

1172 

1173 def put(self, *args: t.Any, **kw: t.Any) -> TestResponse: 

1174 """Call :meth:`open` with ``method`` set to ``PUT``.""" 

1175 kw["method"] = "PUT" 

1176 return self.open(*args, **kw) 

1177 

1178 def delete(self, *args: t.Any, **kw: t.Any) -> TestResponse: 

1179 """Call :meth:`open` with ``method`` set to ``DELETE``.""" 

1180 kw["method"] = "DELETE" 

1181 return self.open(*args, **kw) 

1182 

1183 def patch(self, *args: t.Any, **kw: t.Any) -> TestResponse: 

1184 """Call :meth:`open` with ``method`` set to ``PATCH``.""" 

1185 kw["method"] = "PATCH" 

1186 return self.open(*args, **kw) 

1187 

1188 def options(self, *args: t.Any, **kw: t.Any) -> TestResponse: 

1189 """Call :meth:`open` with ``method`` set to ``OPTIONS``.""" 

1190 kw["method"] = "OPTIONS" 

1191 return self.open(*args, **kw) 

1192 

1193 def head(self, *args: t.Any, **kw: t.Any) -> TestResponse: 

1194 """Call :meth:`open` with ``method`` set to ``HEAD``.""" 

1195 kw["method"] = "HEAD" 

1196 return self.open(*args, **kw) 

1197 

1198 def trace(self, *args: t.Any, **kw: t.Any) -> TestResponse: 

1199 """Call :meth:`open` with ``method`` set to ``TRACE``.""" 

1200 kw["method"] = "TRACE" 

1201 return self.open(*args, **kw) 

1202 

1203 def __repr__(self) -> str: 

1204 return f"<{type(self).__name__} {self.application!r}>" 

1205 

1206 

1207def create_environ(*args: t.Any, **kwargs: t.Any) -> WSGIEnvironment: 

1208 """Create a new WSGI environ dict based on the values passed. The first 

1209 parameter should be the path of the request which defaults to '/'. The 

1210 second one can either be an absolute path (in that case the host is 

1211 localhost:80) or a full path to the request with scheme, netloc port and 

1212 the path to the script. 

1213 

1214 This accepts the same arguments as the :class:`EnvironBuilder` 

1215 constructor. 

1216 

1217 .. versionchanged:: 0.5 

1218 This function is now a thin wrapper over :class:`EnvironBuilder` which 

1219 was added in 0.5. The `headers`, `environ_base`, `environ_overrides` 

1220 and `charset` parameters were added. 

1221 """ 

1222 builder = EnvironBuilder(*args, **kwargs) 

1223 

1224 try: 

1225 return builder.get_environ() 

1226 finally: 

1227 builder.close() 

1228 

1229 

1230def run_wsgi_app( 

1231 app: WSGIApplication, environ: WSGIEnvironment, buffered: bool = False 

1232) -> tuple[t.Iterable[bytes], str, Headers]: 

1233 """Return a tuple in the form (app_iter, status, headers) of the 

1234 application output. This works best if you pass it an application that 

1235 returns an iterator all the time. 

1236 

1237 Sometimes applications may use the `write()` callable returned 

1238 by the `start_response` function. This tries to resolve such edge 

1239 cases automatically. But if you don't get the expected output you 

1240 should set `buffered` to `True` which enforces buffering. 

1241 

1242 If passed an invalid WSGI application the behavior of this function is 

1243 undefined. Never pass non-conforming WSGI applications to this function. 

1244 

1245 :param app: the application to execute. 

1246 :param buffered: set to `True` to enforce buffering. 

1247 :return: tuple in the form ``(app_iter, status, headers)`` 

1248 """ 

1249 # Copy environ to ensure any mutations by the app (ProxyFix, for 

1250 # example) don't affect subsequent requests (such as redirects). 

1251 environ = _get_environ(environ).copy() 

1252 status: str 

1253 response: tuple[str, list[tuple[str, str]]] | None = None 

1254 buffer: list[bytes] = [] 

1255 

1256 def start_response(status, headers, exc_info=None): # type: ignore 

1257 nonlocal response 

1258 

1259 if exc_info: 

1260 try: 

1261 raise exc_info[1].with_traceback(exc_info[2]) 

1262 finally: 

1263 exc_info = None 

1264 

1265 response = (status, headers) 

1266 return buffer.append 

1267 

1268 app_rv = app(environ, start_response) 

1269 close_func = getattr(app_rv, "close", None) 

1270 app_iter: t.Iterable[bytes] = iter(app_rv) 

1271 

1272 # when buffering we emit the close call early and convert the 

1273 # application iterator into a regular list 

1274 if buffered: 

1275 try: 

1276 app_iter = list(app_iter) 

1277 finally: 

1278 if close_func is not None: 

1279 close_func() 

1280 

1281 # otherwise we iterate the application iter until we have a response, chain 

1282 # the already received data with the already collected data and wrap it in 

1283 # a new `ClosingIterator` if we need to restore a `close` callable from the 

1284 # original return value. 

1285 else: 

1286 for item in app_iter: 

1287 buffer.append(item) 

1288 

1289 if response is not None: 

1290 break 

1291 

1292 if buffer: 

1293 app_iter = chain(buffer, app_iter) 

1294 

1295 if close_func is not None and app_iter is not app_rv: 

1296 app_iter = ClosingIterator(app_iter, close_func) 

1297 

1298 status, headers = response # type: ignore 

1299 return app_iter, status, Headers(headers) 

1300 

1301 

1302class TestResponse(Response): 

1303 """:class:`~werkzeug.wrappers.Response` subclass that provides extra 

1304 information about requests made with the test :class:`Client`. 

1305 

1306 Test client requests will always return an instance of this class. 

1307 If a custom response class is passed to the client, it is 

1308 subclassed along with this to support test information. 

1309 

1310 If the test request included large files, or if the application is 

1311 serving a file, call :meth:`close` to close any open files and 

1312 prevent Python showing a ``ResourceWarning``. 

1313 

1314 .. versionchanged:: 2.2 

1315 Set the ``default_mimetype`` to None to prevent a mimetype being 

1316 assumed if missing. 

1317 

1318 .. versionchanged:: 2.1 

1319 Response instances cannot be treated as tuples. 

1320 

1321 .. versionadded:: 2.0 

1322 Test client methods always return instances of this class. 

1323 """ 

1324 

1325 default_mimetype = None 

1326 # Don't assume a mimetype, instead use whatever the response provides 

1327 

1328 request: Request 

1329 """A request object with the environ used to make the request that 

1330 resulted in this response. 

1331 """ 

1332 

1333 history: tuple[TestResponse, ...] 

1334 """A list of intermediate responses. Populated when the test request 

1335 is made with ``follow_redirects`` enabled. 

1336 """ 

1337 

1338 # Tell Pytest to ignore this, it's not a test class. 

1339 __test__ = False 

1340 

1341 def __init__( 

1342 self, 

1343 response: t.Iterable[bytes], 

1344 status: str, 

1345 headers: Headers, 

1346 request: Request, 

1347 history: tuple[TestResponse] = (), # type: ignore 

1348 **kwargs: t.Any, 

1349 ) -> None: 

1350 super().__init__(response, status, headers, **kwargs) 

1351 self.request = request 

1352 self.history = history 

1353 self._compat_tuple = response, status, headers 

1354 

1355 @cached_property 

1356 def text(self) -> str: 

1357 """The response data as text. A shortcut for 

1358 ``response.get_data(as_text=True)``. 

1359 

1360 .. versionadded:: 2.1 

1361 """ 

1362 return self.get_data(as_text=True) 

1363 

1364 

1365@dataclasses.dataclass 

1366class Cookie: 

1367 """A cookie key, value, and parameters. 

1368 

1369 The class itself is not a public API. Its attributes are documented for inspection 

1370 with :meth:`.Client.get_cookie` only. 

1371 

1372 .. versionadded:: 2.3 

1373 """ 

1374 

1375 key: str 

1376 """The cookie key, encoded as a client would see it.""" 

1377 

1378 value: str 

1379 """The cookie key, encoded as a client would see it.""" 

1380 

1381 decoded_key: str 

1382 """The cookie key, decoded as the application would set and see it.""" 

1383 

1384 decoded_value: str 

1385 """The cookie value, decoded as the application would set and see it.""" 

1386 

1387 expires: datetime | None 

1388 """The time at which the cookie is no longer valid.""" 

1389 

1390 max_age: int | None 

1391 """The number of seconds from when the cookie was set at which it is 

1392 no longer valid. 

1393 """ 

1394 

1395 domain: str 

1396 """The domain that the cookie was set for, or the request domain if not set.""" 

1397 

1398 origin_only: bool 

1399 """Whether the cookie will be sent for exact domain matches only. This is ``True`` 

1400 if the ``Domain`` parameter was not present. 

1401 """ 

1402 

1403 path: str 

1404 """The path that the cookie was set for.""" 

1405 

1406 secure: bool | None 

1407 """The ``Secure`` parameter.""" 

1408 

1409 http_only: bool | None 

1410 """The ``HttpOnly`` parameter.""" 

1411 

1412 same_site: str | None 

1413 """The ``SameSite`` parameter.""" 

1414 

1415 def _matches_request(self, server_name: str, path: str) -> bool: 

1416 return ( 

1417 server_name == self.domain 

1418 or ( 

1419 not self.origin_only 

1420 and server_name.endswith(self.domain) 

1421 and server_name[: -len(self.domain)].endswith(".") 

1422 ) 

1423 ) and ( 

1424 path == self.path 

1425 or ( 

1426 path.startswith(self.path) 

1427 and path[len(self.path) - self.path.endswith("/") :].startswith("/") 

1428 ) 

1429 ) 

1430 

1431 def _to_request_header(self) -> str: 

1432 return f"{self.key}={self.value}" 

1433 

1434 @classmethod 

1435 def _from_response_header(cls, server_name: str, path: str, header: str) -> te.Self: 

1436 header, _, parameters_str = header.partition(";") 

1437 key, _, value = header.partition("=") 

1438 decoded_key, decoded_value = next(parse_cookie(header).items()) # type: ignore[call-overload] 

1439 params = {} 

1440 

1441 for item in parameters_str.split(";"): 

1442 k, sep, v = item.partition("=") 

1443 params[k.strip(" \t").lower()] = v.strip(" \t") if sep else None 

1444 

1445 return cls( 

1446 key=key.strip(" \t"), 

1447 value=value.strip(" \t"), 

1448 decoded_key=decoded_key, 

1449 decoded_value=decoded_value, 

1450 expires=parse_date(params.get("expires")), 

1451 max_age=int(params["max-age"] or 0) if "max-age" in params else None, 

1452 domain=params.get("domain") or server_name, 

1453 origin_only="domain" not in params, 

1454 path=params.get("path") or path.rpartition("/")[0] or "/", 

1455 secure="secure" in params, 

1456 http_only="httponly" in params, 

1457 same_site=params.get("samesite"), 

1458 ) 

1459 

1460 @property 

1461 def _storage_key(self) -> tuple[str, str, str]: 

1462 return self.domain, self.path, self.decoded_key 

1463 

1464 @property 

1465 def _should_delete(self) -> bool: 

1466 return self.max_age == 0 or ( 

1467 self.expires is not None and self.expires.timestamp() == 0 

1468 )