1465 lines
		
	
	
	
		
			52 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
		
		
			
		
	
	
			1465 lines
		
	
	
	
		
			52 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
|   | from __future__ import annotations | ||
|  | 
 | ||
|  | import dataclasses | ||
|  | import mimetypes | ||
|  | import sys | ||
|  | import typing as t | ||
|  | from collections import defaultdict | ||
|  | from datetime import datetime | ||
|  | from io import BytesIO | ||
|  | from itertools import chain | ||
|  | from random import random | ||
|  | from tempfile import TemporaryFile | ||
|  | from time import time | ||
|  | from urllib.parse import unquote | ||
|  | from urllib.parse import urlsplit | ||
|  | from urllib.parse import urlunsplit | ||
|  | 
 | ||
|  | from ._internal import _get_environ | ||
|  | from ._internal import _wsgi_decoding_dance | ||
|  | from ._internal import _wsgi_encoding_dance | ||
|  | from .datastructures import Authorization | ||
|  | from .datastructures import CallbackDict | ||
|  | from .datastructures import CombinedMultiDict | ||
|  | from .datastructures import EnvironHeaders | ||
|  | from .datastructures import FileMultiDict | ||
|  | from .datastructures import Headers | ||
|  | from .datastructures import MultiDict | ||
|  | from .http import dump_cookie | ||
|  | from .http import dump_options_header | ||
|  | from .http import parse_cookie | ||
|  | from .http import parse_date | ||
|  | from .http import parse_options_header | ||
|  | from .sansio.multipart import Data | ||
|  | from .sansio.multipart import Epilogue | ||
|  | from .sansio.multipart import Field | ||
|  | from .sansio.multipart import File | ||
|  | from .sansio.multipart import MultipartEncoder | ||
|  | from .sansio.multipart import Preamble | ||
|  | from .urls import _urlencode | ||
|  | from .urls import iri_to_uri | ||
|  | from .utils import cached_property | ||
|  | from .utils import get_content_type | ||
|  | from .wrappers.request import Request | ||
|  | from .wrappers.response import Response | ||
|  | from .wsgi import ClosingIterator | ||
|  | from .wsgi import get_current_url | ||
|  | 
 | ||
|  | if t.TYPE_CHECKING: | ||
|  |     import typing_extensions as te | ||
|  |     from _typeshed.wsgi import WSGIApplication | ||
|  |     from _typeshed.wsgi import WSGIEnvironment | ||
|  | 
 | ||
|  | 
 | ||
|  | def stream_encode_multipart( | ||
|  |     data: t.Mapping[str, t.Any], | ||
|  |     use_tempfile: bool = True, | ||
|  |     threshold: int = 1024 * 500, | ||
|  |     boundary: str | None = None, | ||
|  | ) -> tuple[t.IO[bytes], int, str]: | ||
|  |     """Encode a dict of values (either strings or file descriptors or
 | ||
|  |     :class:`FileStorage` objects.) into a multipart encoded string stored | ||
|  |     in a file descriptor. | ||
|  | 
 | ||
|  |     .. versionchanged:: 3.0 | ||
|  |         The ``charset`` parameter was removed. | ||
|  |     """
 | ||
|  |     if boundary is None: | ||
|  |         boundary = f"---------------WerkzeugFormPart_{time()}{random()}" | ||
|  | 
 | ||
|  |     stream: t.IO[bytes] = BytesIO() | ||
|  |     total_length = 0 | ||
|  |     on_disk = False | ||
|  |     write_binary: t.Callable[[bytes], int] | ||
|  | 
 | ||
|  |     if use_tempfile: | ||
|  | 
 | ||
|  |         def write_binary(s: bytes) -> int: | ||
|  |             nonlocal stream, total_length, on_disk | ||
|  | 
 | ||
|  |             if on_disk: | ||
|  |                 return stream.write(s) | ||
|  |             else: | ||
|  |                 length = len(s) | ||
|  | 
 | ||
|  |                 if length + total_length <= threshold: | ||
|  |                     stream.write(s) | ||
|  |                 else: | ||
|  |                     new_stream = t.cast(t.IO[bytes], TemporaryFile("wb+")) | ||
|  |                     new_stream.write(stream.getvalue())  # type: ignore | ||
|  |                     new_stream.write(s) | ||
|  |                     stream = new_stream | ||
|  |                     on_disk = True | ||
|  | 
 | ||
|  |                 total_length += length | ||
|  |                 return length | ||
|  | 
 | ||
|  |     else: | ||
|  |         write_binary = stream.write | ||
|  | 
 | ||
|  |     encoder = MultipartEncoder(boundary.encode()) | ||
|  |     write_binary(encoder.send_event(Preamble(data=b""))) | ||
|  |     for key, value in _iter_data(data): | ||
|  |         reader = getattr(value, "read", None) | ||
|  |         if reader is not None: | ||
|  |             filename = getattr(value, "filename", getattr(value, "name", None)) | ||
|  |             content_type = getattr(value, "content_type", None) | ||
|  |             if content_type is None: | ||
|  |                 content_type = ( | ||
|  |                     filename | ||
|  |                     and mimetypes.guess_type(filename)[0] | ||
|  |                     or "application/octet-stream" | ||
|  |                 ) | ||
|  |             headers = value.headers | ||
|  |             headers.update([("Content-Type", content_type)]) | ||
|  |             if filename is None: | ||
|  |                 write_binary(encoder.send_event(Field(name=key, headers=headers))) | ||
|  |             else: | ||
|  |                 write_binary( | ||
|  |                     encoder.send_event( | ||
|  |                         File(name=key, filename=filename, headers=headers) | ||
|  |                     ) | ||
|  |                 ) | ||
|  |             while True: | ||
|  |                 chunk = reader(16384) | ||
|  | 
 | ||
|  |                 if not chunk: | ||
|  |                     write_binary(encoder.send_event(Data(data=chunk, more_data=False))) | ||
|  |                     break | ||
|  | 
 | ||
|  |                 write_binary(encoder.send_event(Data(data=chunk, more_data=True))) | ||
|  |         else: | ||
|  |             if not isinstance(value, str): | ||
|  |                 value = str(value) | ||
|  |             write_binary(encoder.send_event(Field(name=key, headers=Headers()))) | ||
|  |             write_binary(encoder.send_event(Data(data=value.encode(), more_data=False))) | ||
|  | 
 | ||
|  |     write_binary(encoder.send_event(Epilogue(data=b""))) | ||
|  | 
 | ||
|  |     length = stream.tell() | ||
|  |     stream.seek(0) | ||
|  |     return stream, length, boundary | ||
|  | 
 | ||
|  | 
 | ||
|  | def encode_multipart( | ||
|  |     values: t.Mapping[str, t.Any], boundary: str | None = None | ||
|  | ) -> tuple[str, bytes]: | ||
|  |     """Like `stream_encode_multipart` but returns a tuple in the form
 | ||
|  |     (``boundary``, ``data``) where data is bytes. | ||
|  | 
 | ||
|  |     .. versionchanged:: 3.0 | ||
|  |         The ``charset`` parameter was removed. | ||
|  |     """
 | ||
|  |     stream, length, boundary = stream_encode_multipart( | ||
|  |         values, use_tempfile=False, boundary=boundary | ||
|  |     ) | ||
|  |     return boundary, stream.read() | ||
|  | 
 | ||
|  | 
 | ||
|  | def _iter_data(data: t.Mapping[str, t.Any]) -> t.Iterator[tuple[str, t.Any]]: | ||
|  |     """Iterate over a mapping that might have a list of values, yielding
 | ||
|  |     all key, value pairs. Almost like iter_multi_items but only allows | ||
|  |     lists, not tuples, of values so tuples can be used for files. | ||
|  |     """
 | ||
|  |     if isinstance(data, MultiDict): | ||
|  |         yield from data.items(multi=True) | ||
|  |     else: | ||
|  |         for key, value in data.items(): | ||
|  |             if isinstance(value, list): | ||
|  |                 for v in value: | ||
|  |                     yield key, v | ||
|  |             else: | ||
|  |                 yield key, value | ||
|  | 
 | ||
|  | 
 | ||
|  | _TAnyMultiDict = t.TypeVar("_TAnyMultiDict", bound="MultiDict[t.Any, t.Any]") | ||
|  | 
 | ||
|  | 
 | ||
|  | class EnvironBuilder: | ||
|  |     """This class can be used to conveniently create a WSGI environment
 | ||
|  |     for testing purposes.  It can be used to quickly create WSGI environments | ||
|  |     or request objects from arbitrary data. | ||
|  | 
 | ||
|  |     The signature of this class is also used in some other places as of | ||
|  |     Werkzeug 0.5 (:func:`create_environ`, :meth:`Response.from_values`, | ||
|  |     :meth:`Client.open`).  Because of this most of the functionality is | ||
|  |     available through the constructor alone. | ||
|  | 
 | ||
|  |     Files and regular form data can be manipulated independently of each | ||
|  |     other with the :attr:`form` and :attr:`files` attributes, but are | ||
|  |     passed with the same argument to the constructor: `data`. | ||
|  | 
 | ||
|  |     `data` can be any of these values: | ||
|  | 
 | ||
|  |     -   a `str` or `bytes` object: The object is converted into an | ||
|  |         :attr:`input_stream`, the :attr:`content_length` is set and you have to | ||
|  |         provide a :attr:`content_type`. | ||
|  |     -   a `dict` or :class:`MultiDict`: The keys have to be strings. The values | ||
|  |         have to be either any of the following objects, or a list of any of the | ||
|  |         following objects: | ||
|  | 
 | ||
|  |         -   a :class:`file`-like object:  These are converted into | ||
|  |             :class:`FileStorage` objects automatically. | ||
|  |         -   a `tuple`:  The :meth:`~FileMultiDict.add_file` method is called | ||
|  |             with the key and the unpacked `tuple` items as positional | ||
|  |             arguments. | ||
|  |         -   a `str`:  The string is set as form data for the associated key. | ||
|  |     -   a file-like object: The object content is loaded in memory and then | ||
|  |         handled like a regular `str` or a `bytes`. | ||
|  | 
 | ||
|  |     :param path: the path of the request.  In the WSGI environment this will | ||
|  |                  end up as `PATH_INFO`.  If the `query_string` is not defined | ||
|  |                  and there is a question mark in the `path` everything after | ||
|  |                  it is used as query string. | ||
|  |     :param base_url: the base URL is a URL that is used to extract the WSGI | ||
|  |                      URL scheme, host (server name + server port) and the | ||
|  |                      script root (`SCRIPT_NAME`). | ||
|  |     :param query_string: an optional string or dict with URL parameters. | ||
|  |     :param method: the HTTP method to use, defaults to `GET`. | ||
|  |     :param input_stream: an optional input stream.  Do not specify this and | ||
|  |                          `data`.  As soon as an input stream is set you can't | ||
|  |                          modify :attr:`args` and :attr:`files` unless you | ||
|  |                          set the :attr:`input_stream` to `None` again. | ||
|  |     :param content_type: The content type for the request.  As of 0.5 you | ||
|  |                          don't have to provide this when specifying files | ||
|  |                          and form data via `data`. | ||
|  |     :param content_length: The content length for the request.  You don't | ||
|  |                            have to specify this when providing data via | ||
|  |                            `data`. | ||
|  |     :param errors_stream: an optional error stream that is used for | ||
|  |                           `wsgi.errors`.  Defaults to :data:`stderr`. | ||
|  |     :param multithread: controls `wsgi.multithread`.  Defaults to `False`. | ||
|  |     :param multiprocess: controls `wsgi.multiprocess`.  Defaults to `False`. | ||
|  |     :param run_once: controls `wsgi.run_once`.  Defaults to `False`. | ||
|  |     :param headers: an optional list or :class:`Headers` object of headers. | ||
|  |     :param data: a string or dict of form data or a file-object. | ||
|  |                  See explanation above. | ||
|  |     :param json: An object to be serialized and assigned to ``data``. | ||
|  |         Defaults the content type to ``"application/json"``. | ||
|  |         Serialized with the function assigned to :attr:`json_dumps`. | ||
|  |     :param environ_base: an optional dict of environment defaults. | ||
|  |     :param environ_overrides: an optional dict of environment overrides. | ||
|  |     :param auth: An authorization object to use for the | ||
|  |         ``Authorization`` header value. A ``(username, password)`` tuple | ||
|  |         is a shortcut for ``Basic`` authorization. | ||
|  | 
 | ||
|  |     .. versionchanged:: 3.0 | ||
|  |         The ``charset`` parameter was removed. | ||
|  | 
 | ||
|  |     .. versionchanged:: 2.1 | ||
|  |         ``CONTENT_TYPE`` and ``CONTENT_LENGTH`` are not duplicated as | ||
|  |         header keys in the environ. | ||
|  | 
 | ||
|  |     .. versionchanged:: 2.0 | ||
|  |         ``REQUEST_URI`` and ``RAW_URI`` is the full raw URI including | ||
|  |         the query string, not only the path. | ||
|  | 
 | ||
|  |     .. versionchanged:: 2.0 | ||
|  |         The default :attr:`request_class` is ``Request`` instead of | ||
|  |         ``BaseRequest``. | ||
|  | 
 | ||
|  |     .. versionadded:: 2.0 | ||
|  |        Added the ``auth`` parameter. | ||
|  | 
 | ||
|  |     .. versionadded:: 0.15 | ||
|  |         The ``json`` param and :meth:`json_dumps` method. | ||
|  | 
 | ||
|  |     .. versionadded:: 0.15 | ||
|  |         The environ has keys ``REQUEST_URI`` and ``RAW_URI`` containing | ||
|  |         the path before percent-decoding. This is not part of the WSGI | ||
|  |         PEP, but many WSGI servers include it. | ||
|  | 
 | ||
|  |     .. versionchanged:: 0.6 | ||
|  |        ``path`` and ``base_url`` can now be unicode strings that are | ||
|  |        encoded with :func:`iri_to_uri`. | ||
|  |     """
 | ||
|  | 
 | ||
|  |     #: the server protocol to use.  defaults to HTTP/1.1 | ||
|  |     server_protocol = "HTTP/1.1" | ||
|  | 
 | ||
|  |     #: the wsgi version to use.  defaults to (1, 0) | ||
|  |     wsgi_version = (1, 0) | ||
|  | 
 | ||
|  |     #: The default request class used by :meth:`get_request`. | ||
|  |     request_class = Request | ||
|  | 
 | ||
|  |     import json | ||
|  | 
 | ||
|  |     #: The serialization function used when ``json`` is passed. | ||
|  |     json_dumps = staticmethod(json.dumps) | ||
|  |     del json | ||
|  | 
 | ||
|  |     _args: MultiDict[str, str] | None | ||
|  |     _query_string: str | None | ||
|  |     _input_stream: t.IO[bytes] | None | ||
|  |     _form: MultiDict[str, str] | None | ||
|  |     _files: FileMultiDict | None | ||
|  | 
 | ||
|  |     def __init__( | ||
|  |         self, | ||
|  |         path: str = "/", | ||
|  |         base_url: str | None = None, | ||
|  |         query_string: t.Mapping[str, str] | str | None = None, | ||
|  |         method: str = "GET", | ||
|  |         input_stream: t.IO[bytes] | None = None, | ||
|  |         content_type: str | None = None, | ||
|  |         content_length: int | None = None, | ||
|  |         errors_stream: t.IO[str] | None = None, | ||
|  |         multithread: bool = False, | ||
|  |         multiprocess: bool = False, | ||
|  |         run_once: bool = False, | ||
|  |         headers: Headers | t.Iterable[tuple[str, str]] | None = None, | ||
|  |         data: None | (t.IO[bytes] | str | bytes | t.Mapping[str, t.Any]) = None, | ||
|  |         environ_base: t.Mapping[str, t.Any] | None = None, | ||
|  |         environ_overrides: t.Mapping[str, t.Any] | None = None, | ||
|  |         mimetype: str | None = None, | ||
|  |         json: t.Mapping[str, t.Any] | None = None, | ||
|  |         auth: Authorization | tuple[str, str] | None = None, | ||
|  |     ) -> None: | ||
|  |         if query_string is not None and "?" in path: | ||
|  |             raise ValueError("Query string is defined in the path and as an argument") | ||
|  |         request_uri = urlsplit(path) | ||
|  |         if query_string is None and "?" in path: | ||
|  |             query_string = request_uri.query | ||
|  | 
 | ||
|  |         self.path = iri_to_uri(request_uri.path) | ||
|  |         self.request_uri = path | ||
|  |         if base_url is not None: | ||
|  |             base_url = iri_to_uri(base_url) | ||
|  |         self.base_url = base_url  # type: ignore | ||
|  |         if isinstance(query_string, str): | ||
|  |             self.query_string = query_string | ||
|  |         else: | ||
|  |             if query_string is None: | ||
|  |                 query_string = MultiDict() | ||
|  |             elif not isinstance(query_string, MultiDict): | ||
|  |                 query_string = MultiDict(query_string) | ||
|  |             self.args = query_string | ||
|  |         self.method = method | ||
|  |         if headers is None: | ||
|  |             headers = Headers() | ||
|  |         elif not isinstance(headers, Headers): | ||
|  |             headers = Headers(headers) | ||
|  |         self.headers = headers | ||
|  |         if content_type is not None: | ||
|  |             self.content_type = content_type | ||
|  |         if errors_stream is None: | ||
|  |             errors_stream = sys.stderr | ||
|  |         self.errors_stream = errors_stream | ||
|  |         self.multithread = multithread | ||
|  |         self.multiprocess = multiprocess | ||
|  |         self.run_once = run_once | ||
|  |         self.environ_base = environ_base | ||
|  |         self.environ_overrides = environ_overrides | ||
|  |         self.input_stream = input_stream | ||
|  |         self.content_length = content_length | ||
|  |         self.closed = False | ||
|  | 
 | ||
|  |         if auth is not None: | ||
|  |             if isinstance(auth, tuple): | ||
|  |                 auth = Authorization( | ||
|  |                     "basic", {"username": auth[0], "password": auth[1]} | ||
|  |                 ) | ||
|  | 
 | ||
|  |             self.headers.set("Authorization", auth.to_header()) | ||
|  | 
 | ||
|  |         if json is not None: | ||
|  |             if data is not None: | ||
|  |                 raise TypeError("can't provide both json and data") | ||
|  | 
 | ||
|  |             data = self.json_dumps(json) | ||
|  | 
 | ||
|  |             if self.content_type is None: | ||
|  |                 self.content_type = "application/json" | ||
|  | 
 | ||
|  |         if data: | ||
|  |             if input_stream is not None: | ||
|  |                 raise TypeError("can't provide input stream and data") | ||
|  |             if hasattr(data, "read"): | ||
|  |                 data = data.read() | ||
|  |             if isinstance(data, str): | ||
|  |                 data = data.encode() | ||
|  |             if isinstance(data, bytes): | ||
|  |                 self.input_stream = BytesIO(data) | ||
|  |                 if self.content_length is None: | ||
|  |                     self.content_length = len(data) | ||
|  |             else: | ||
|  |                 for key, value in _iter_data(data): | ||
|  |                     if isinstance(value, (tuple, dict)) or hasattr(value, "read"): | ||
|  |                         self._add_file_from_data(key, value) | ||
|  |                     else: | ||
|  |                         self.form.setlistdefault(key).append(value) | ||
|  | 
 | ||
|  |         if mimetype is not None: | ||
|  |             self.mimetype = mimetype | ||
|  | 
 | ||
|  |     @classmethod | ||
|  |     def from_environ(cls, environ: WSGIEnvironment, **kwargs: t.Any) -> EnvironBuilder: | ||
|  |         """Turn an environ dict back into a builder. Any extra kwargs
 | ||
|  |         override the args extracted from the environ. | ||
|  | 
 | ||
|  |         .. versionchanged:: 2.0 | ||
|  |             Path and query values are passed through the WSGI decoding | ||
|  |             dance to avoid double encoding. | ||
|  | 
 | ||
|  |         .. versionadded:: 0.15 | ||
|  |         """
 | ||
|  |         headers = Headers(EnvironHeaders(environ)) | ||
|  |         out = { | ||
|  |             "path": _wsgi_decoding_dance(environ["PATH_INFO"]), | ||
|  |             "base_url": cls._make_base_url( | ||
|  |                 environ["wsgi.url_scheme"], | ||
|  |                 headers.pop("Host"), | ||
|  |                 _wsgi_decoding_dance(environ["SCRIPT_NAME"]), | ||
|  |             ), | ||
|  |             "query_string": _wsgi_decoding_dance(environ["QUERY_STRING"]), | ||
|  |             "method": environ["REQUEST_METHOD"], | ||
|  |             "input_stream": environ["wsgi.input"], | ||
|  |             "content_type": headers.pop("Content-Type", None), | ||
|  |             "content_length": headers.pop("Content-Length", None), | ||
|  |             "errors_stream": environ["wsgi.errors"], | ||
|  |             "multithread": environ["wsgi.multithread"], | ||
|  |             "multiprocess": environ["wsgi.multiprocess"], | ||
|  |             "run_once": environ["wsgi.run_once"], | ||
|  |             "headers": headers, | ||
|  |         } | ||
|  |         out.update(kwargs) | ||
|  |         return cls(**out) | ||
|  | 
 | ||
|  |     def _add_file_from_data( | ||
|  |         self, | ||
|  |         key: str, | ||
|  |         value: (t.IO[bytes] | tuple[t.IO[bytes], str] | tuple[t.IO[bytes], str, str]), | ||
|  |     ) -> None: | ||
|  |         """Called in the EnvironBuilder to add files from the data dict.""" | ||
|  |         if isinstance(value, tuple): | ||
|  |             self.files.add_file(key, *value) | ||
|  |         else: | ||
|  |             self.files.add_file(key, value) | ||
|  | 
 | ||
|  |     @staticmethod | ||
|  |     def _make_base_url(scheme: str, host: str, script_root: str) -> str: | ||
|  |         return urlunsplit((scheme, host, script_root, "", "")).rstrip("/") + "/" | ||
|  | 
 | ||
|  |     @property | ||
|  |     def base_url(self) -> str: | ||
|  |         """The base URL is used to extract the URL scheme, host name,
 | ||
|  |         port, and root path. | ||
|  |         """
 | ||
|  |         return self._make_base_url(self.url_scheme, self.host, self.script_root) | ||
|  | 
 | ||
|  |     @base_url.setter | ||
|  |     def base_url(self, value: str | None) -> None: | ||
|  |         if value is None: | ||
|  |             scheme = "http" | ||
|  |             netloc = "localhost" | ||
|  |             script_root = "" | ||
|  |         else: | ||
|  |             scheme, netloc, script_root, qs, anchor = urlsplit(value) | ||
|  |             if qs or anchor: | ||
|  |                 raise ValueError("base url must not contain a query string or fragment") | ||
|  |         self.script_root = script_root.rstrip("/") | ||
|  |         self.host = netloc | ||
|  |         self.url_scheme = scheme | ||
|  | 
 | ||
|  |     @property | ||
|  |     def content_type(self) -> str | None: | ||
|  |         """The content type for the request.  Reflected from and to
 | ||
|  |         the :attr:`headers`.  Do not set if you set :attr:`files` or | ||
|  |         :attr:`form` for auto detection. | ||
|  |         """
 | ||
|  |         ct = self.headers.get("Content-Type") | ||
|  |         if ct is None and not self._input_stream: | ||
|  |             if self._files: | ||
|  |                 return "multipart/form-data" | ||
|  |             if self._form: | ||
|  |                 return "application/x-www-form-urlencoded" | ||
|  |             return None | ||
|  |         return ct | ||
|  | 
 | ||
|  |     @content_type.setter | ||
|  |     def content_type(self, value: str | None) -> None: | ||
|  |         if value is None: | ||
|  |             self.headers.pop("Content-Type", None) | ||
|  |         else: | ||
|  |             self.headers["Content-Type"] = value | ||
|  | 
 | ||
|  |     @property | ||
|  |     def mimetype(self) -> str | None: | ||
|  |         """The mimetype (content type without charset etc.)
 | ||
|  | 
 | ||
|  |         .. versionadded:: 0.14 | ||
|  |         """
 | ||
|  |         ct = self.content_type | ||
|  |         return ct.split(";")[0].strip() if ct else None | ||
|  | 
 | ||
|  |     @mimetype.setter | ||
|  |     def mimetype(self, value: str) -> None: | ||
|  |         self.content_type = get_content_type(value, "utf-8") | ||
|  | 
 | ||
|  |     @property | ||
|  |     def mimetype_params(self) -> t.Mapping[str, str]: | ||
|  |         """The mimetype parameters as dict.  For example if the
 | ||
|  |         content type is ``text/html; charset=utf-8`` the params would be | ||
|  |         ``{'charset': 'utf-8'}``. | ||
|  | 
 | ||
|  |         .. versionadded:: 0.14 | ||
|  |         """
 | ||
|  | 
 | ||
|  |         def on_update(d: CallbackDict[str, str]) -> None: | ||
|  |             self.headers["Content-Type"] = dump_options_header(self.mimetype, d) | ||
|  | 
 | ||
|  |         d = parse_options_header(self.headers.get("content-type", ""))[1] | ||
|  |         return CallbackDict(d, on_update) | ||
|  | 
 | ||
|  |     @property | ||
|  |     def content_length(self) -> int | None: | ||
|  |         """The content length as integer.  Reflected from and to the
 | ||
|  |         :attr:`headers`.  Do not set if you set :attr:`files` or | ||
|  |         :attr:`form` for auto detection. | ||
|  |         """
 | ||
|  |         return self.headers.get("Content-Length", type=int) | ||
|  | 
 | ||
|  |     @content_length.setter | ||
|  |     def content_length(self, value: int | None) -> None: | ||
|  |         if value is None: | ||
|  |             self.headers.pop("Content-Length", None) | ||
|  |         else: | ||
|  |             self.headers["Content-Length"] = str(value) | ||
|  | 
 | ||
|  |     def _get_form(self, name: str, storage: type[_TAnyMultiDict]) -> _TAnyMultiDict: | ||
|  |         """Common behavior for getting the :attr:`form` and
 | ||
|  |         :attr:`files` properties. | ||
|  | 
 | ||
|  |         :param name: Name of the internal cached attribute. | ||
|  |         :param storage: Storage class used for the data. | ||
|  |         """
 | ||
|  |         if self.input_stream is not None: | ||
|  |             raise AttributeError("an input stream is defined") | ||
|  | 
 | ||
|  |         rv = getattr(self, name) | ||
|  | 
 | ||
|  |         if rv is None: | ||
|  |             rv = storage() | ||
|  |             setattr(self, name, rv) | ||
|  | 
 | ||
|  |         return rv  # type: ignore | ||
|  | 
 | ||
|  |     def _set_form(self, name: str, value: MultiDict[str, t.Any]) -> None: | ||
|  |         """Common behavior for setting the :attr:`form` and
 | ||
|  |         :attr:`files` properties. | ||
|  | 
 | ||
|  |         :param name: Name of the internal cached attribute. | ||
|  |         :param value: Value to assign to the attribute. | ||
|  |         """
 | ||
|  |         self._input_stream = None | ||
|  |         setattr(self, name, value) | ||
|  | 
 | ||
|  |     @property | ||
|  |     def form(self) -> MultiDict[str, str]: | ||
|  |         """A :class:`MultiDict` of form values.""" | ||
|  |         return self._get_form("_form", MultiDict) | ||
|  | 
 | ||
|  |     @form.setter | ||
|  |     def form(self, value: MultiDict[str, str]) -> None: | ||
|  |         self._set_form("_form", value) | ||
|  | 
 | ||
|  |     @property | ||
|  |     def files(self) -> FileMultiDict: | ||
|  |         """A :class:`FileMultiDict` of uploaded files. Use
 | ||
|  |         :meth:`~FileMultiDict.add_file` to add new files. | ||
|  |         """
 | ||
|  |         return self._get_form("_files", FileMultiDict) | ||
|  | 
 | ||
|  |     @files.setter | ||
|  |     def files(self, value: FileMultiDict) -> None: | ||
|  |         self._set_form("_files", value) | ||
|  | 
 | ||
|  |     @property | ||
|  |     def input_stream(self) -> t.IO[bytes] | None: | ||
|  |         """An optional input stream. This is mutually exclusive with
 | ||
|  |         setting :attr:`form` and :attr:`files`, setting it will clear | ||
|  |         those. Do not provide this if the method is not ``POST`` or | ||
|  |         another method that has a body. | ||
|  |         """
 | ||
|  |         return self._input_stream | ||
|  | 
 | ||
|  |     @input_stream.setter | ||
|  |     def input_stream(self, value: t.IO[bytes] | None) -> None: | ||
|  |         self._input_stream = value | ||
|  |         self._form = None | ||
|  |         self._files = None | ||
|  | 
 | ||
|  |     @property | ||
|  |     def query_string(self) -> str: | ||
|  |         """The query string.  If you set this to a string
 | ||
|  |         :attr:`args` will no longer be available. | ||
|  |         """
 | ||
|  |         if self._query_string is None: | ||
|  |             if self._args is not None: | ||
|  |                 return _urlencode(self._args) | ||
|  |             return "" | ||
|  |         return self._query_string | ||
|  | 
 | ||
|  |     @query_string.setter | ||
|  |     def query_string(self, value: str | None) -> None: | ||
|  |         self._query_string = value | ||
|  |         self._args = None | ||
|  | 
 | ||
|  |     @property | ||
|  |     def args(self) -> MultiDict[str, str]: | ||
|  |         """The URL arguments as :class:`MultiDict`.""" | ||
|  |         if self._query_string is not None: | ||
|  |             raise AttributeError("a query string is defined") | ||
|  |         if self._args is None: | ||
|  |             self._args = MultiDict() | ||
|  |         return self._args | ||
|  | 
 | ||
|  |     @args.setter | ||
|  |     def args(self, value: MultiDict[str, str] | None) -> None: | ||
|  |         self._query_string = None | ||
|  |         self._args = value | ||
|  | 
 | ||
|  |     @property | ||
|  |     def server_name(self) -> str: | ||
|  |         """The server name (read-only, use :attr:`host` to set)""" | ||
|  |         return self.host.split(":", 1)[0] | ||
|  | 
 | ||
|  |     @property | ||
|  |     def server_port(self) -> int: | ||
|  |         """The server port as integer (read-only, use :attr:`host` to set)""" | ||
|  |         pieces = self.host.split(":", 1) | ||
|  | 
 | ||
|  |         if len(pieces) == 2: | ||
|  |             try: | ||
|  |                 return int(pieces[1]) | ||
|  |             except ValueError: | ||
|  |                 pass | ||
|  | 
 | ||
|  |         if self.url_scheme == "https": | ||
|  |             return 443 | ||
|  |         return 80 | ||
|  | 
 | ||
|  |     def __del__(self) -> None: | ||
|  |         try: | ||
|  |             self.close() | ||
|  |         except Exception: | ||
|  |             pass | ||
|  | 
 | ||
|  |     def close(self) -> None: | ||
|  |         """Closes all files.  If you put real :class:`file` objects into the
 | ||
|  |         :attr:`files` dict you can call this method to automatically close | ||
|  |         them all in one go. | ||
|  |         """
 | ||
|  |         if self.closed: | ||
|  |             return | ||
|  |         try: | ||
|  |             files = self.files.values() | ||
|  |         except AttributeError: | ||
|  |             files = () | ||
|  |         for f in files: | ||
|  |             try: | ||
|  |                 f.close() | ||
|  |             except Exception: | ||
|  |                 pass | ||
|  |         self.closed = True | ||
|  | 
 | ||
|  |     def get_environ(self) -> WSGIEnvironment: | ||
|  |         """Return the built environ.
 | ||
|  | 
 | ||
|  |         .. versionchanged:: 0.15 | ||
|  |             The content type and length headers are set based on | ||
|  |             input stream detection. Previously this only set the WSGI | ||
|  |             keys. | ||
|  |         """
 | ||
|  |         input_stream = self.input_stream | ||
|  |         content_length = self.content_length | ||
|  | 
 | ||
|  |         mimetype = self.mimetype | ||
|  |         content_type = self.content_type | ||
|  | 
 | ||
|  |         if input_stream is not None: | ||
|  |             start_pos = input_stream.tell() | ||
|  |             input_stream.seek(0, 2) | ||
|  |             end_pos = input_stream.tell() | ||
|  |             input_stream.seek(start_pos) | ||
|  |             content_length = end_pos - start_pos | ||
|  |         elif mimetype == "multipart/form-data": | ||
|  |             input_stream, content_length, boundary = stream_encode_multipart( | ||
|  |                 CombinedMultiDict([self.form, self.files]) | ||
|  |             ) | ||
|  |             content_type = f'{mimetype}; boundary="{boundary}"' | ||
|  |         elif mimetype == "application/x-www-form-urlencoded": | ||
|  |             form_encoded = _urlencode(self.form).encode("ascii") | ||
|  |             content_length = len(form_encoded) | ||
|  |             input_stream = BytesIO(form_encoded) | ||
|  |         else: | ||
|  |             input_stream = BytesIO() | ||
|  | 
 | ||
|  |         result: WSGIEnvironment = {} | ||
|  |         if self.environ_base: | ||
|  |             result.update(self.environ_base) | ||
|  | 
 | ||
|  |         def _path_encode(x: str) -> str: | ||
|  |             return _wsgi_encoding_dance(unquote(x)) | ||
|  | 
 | ||
|  |         raw_uri = _wsgi_encoding_dance(self.request_uri) | ||
|  |         result.update( | ||
|  |             { | ||
|  |                 "REQUEST_METHOD": self.method, | ||
|  |                 "SCRIPT_NAME": _path_encode(self.script_root), | ||
|  |                 "PATH_INFO": _path_encode(self.path), | ||
|  |                 "QUERY_STRING": _wsgi_encoding_dance(self.query_string), | ||
|  |                 # Non-standard, added by mod_wsgi, uWSGI | ||
|  |                 "REQUEST_URI": raw_uri, | ||
|  |                 # Non-standard, added by gunicorn | ||
|  |                 "RAW_URI": raw_uri, | ||
|  |                 "SERVER_NAME": self.server_name, | ||
|  |                 "SERVER_PORT": str(self.server_port), | ||
|  |                 "HTTP_HOST": self.host, | ||
|  |                 "SERVER_PROTOCOL": self.server_protocol, | ||
|  |                 "wsgi.version": self.wsgi_version, | ||
|  |                 "wsgi.url_scheme": self.url_scheme, | ||
|  |                 "wsgi.input": input_stream, | ||
|  |                 "wsgi.errors": self.errors_stream, | ||
|  |                 "wsgi.multithread": self.multithread, | ||
|  |                 "wsgi.multiprocess": self.multiprocess, | ||
|  |                 "wsgi.run_once": self.run_once, | ||
|  |             } | ||
|  |         ) | ||
|  | 
 | ||
|  |         headers = self.headers.copy() | ||
|  |         # Don't send these as headers, they're part of the environ. | ||
|  |         headers.remove("Content-Type") | ||
|  |         headers.remove("Content-Length") | ||
|  | 
 | ||
|  |         if content_type is not None: | ||
|  |             result["CONTENT_TYPE"] = content_type | ||
|  | 
 | ||
|  |         if content_length is not None: | ||
|  |             result["CONTENT_LENGTH"] = str(content_length) | ||
|  | 
 | ||
|  |         combined_headers = defaultdict(list) | ||
|  | 
 | ||
|  |         for key, value in headers.to_wsgi_list(): | ||
|  |             combined_headers[f"HTTP_{key.upper().replace('-', '_')}"].append(value) | ||
|  | 
 | ||
|  |         for key, values in combined_headers.items(): | ||
|  |             result[key] = ", ".join(values) | ||
|  | 
 | ||
|  |         if self.environ_overrides: | ||
|  |             result.update(self.environ_overrides) | ||
|  | 
 | ||
|  |         return result | ||
|  | 
 | ||
|  |     def get_request(self, cls: type[Request] | None = None) -> Request: | ||
|  |         """Returns a request with the data.  If the request class is not
 | ||
|  |         specified :attr:`request_class` is used. | ||
|  | 
 | ||
|  |         :param cls: The request wrapper to use. | ||
|  |         """
 | ||
|  |         if cls is None: | ||
|  |             cls = self.request_class | ||
|  | 
 | ||
|  |         return cls(self.get_environ()) | ||
|  | 
 | ||
|  | 
 | ||
|  | class ClientRedirectError(Exception): | ||
|  |     """If a redirect loop is detected when using follow_redirects=True with
 | ||
|  |     the :cls:`Client`, then this exception is raised. | ||
|  |     """
 | ||
|  | 
 | ||
|  | 
 | ||
|  | class Client: | ||
|  |     """Simulate sending requests to a WSGI application without running a WSGI or HTTP
 | ||
|  |     server. | ||
|  | 
 | ||
|  |     :param application: The WSGI application to make requests to. | ||
|  |     :param response_wrapper: A :class:`.Response` class to wrap response data with. | ||
|  |         Defaults to :class:`.TestResponse`. If it's not a subclass of ``TestResponse``, | ||
|  |         one will be created. | ||
|  |     :param use_cookies: Persist cookies from ``Set-Cookie`` response headers to the | ||
|  |         ``Cookie`` header in subsequent requests. Domain and path matching is supported, | ||
|  |         but other cookie parameters are ignored. | ||
|  |     :param allow_subdomain_redirects: Allow requests to follow redirects to subdomains. | ||
|  |         Enable this if the application handles subdomains and redirects between them. | ||
|  | 
 | ||
|  |     .. versionchanged:: 2.3 | ||
|  |         Simplify cookie implementation, support domain and path matching. | ||
|  | 
 | ||
|  |     .. versionchanged:: 2.1 | ||
|  |         All data is available as properties on the returned response object. The | ||
|  |         response cannot be returned as a tuple. | ||
|  | 
 | ||
|  |     .. versionchanged:: 2.0 | ||
|  |         ``response_wrapper`` is always a subclass of :class:``TestResponse``. | ||
|  | 
 | ||
|  |     .. versionchanged:: 0.5 | ||
|  |         Added the ``use_cookies`` parameter. | ||
|  |     """
 | ||
|  | 
 | ||
|  |     def __init__( | ||
|  |         self, | ||
|  |         application: WSGIApplication, | ||
|  |         response_wrapper: type[Response] | None = None, | ||
|  |         use_cookies: bool = True, | ||
|  |         allow_subdomain_redirects: bool = False, | ||
|  |     ) -> None: | ||
|  |         self.application = application | ||
|  | 
 | ||
|  |         if response_wrapper in {None, Response}: | ||
|  |             response_wrapper = TestResponse | ||
|  |         elif response_wrapper is not None and not issubclass( | ||
|  |             response_wrapper, TestResponse | ||
|  |         ): | ||
|  |             response_wrapper = type( | ||
|  |                 "WrapperTestResponse", | ||
|  |                 (TestResponse, response_wrapper), | ||
|  |                 {}, | ||
|  |             ) | ||
|  | 
 | ||
|  |         self.response_wrapper = t.cast(type["TestResponse"], response_wrapper) | ||
|  | 
 | ||
|  |         if use_cookies: | ||
|  |             self._cookies: dict[tuple[str, str, str], Cookie] | None = {} | ||
|  |         else: | ||
|  |             self._cookies = None | ||
|  | 
 | ||
|  |         self.allow_subdomain_redirects = allow_subdomain_redirects | ||
|  | 
 | ||
|  |     def get_cookie( | ||
|  |         self, key: str, domain: str = "localhost", path: str = "/" | ||
|  |     ) -> Cookie | None: | ||
|  |         """Return a :class:`.Cookie` if it exists. Cookies are uniquely identified by
 | ||
|  |         ``(domain, path, key)``. | ||
|  | 
 | ||
|  |         :param key: The decoded form of the key for the cookie. | ||
|  |         :param domain: The domain the cookie was set for. | ||
|  |         :param path: The path the cookie was set for. | ||
|  | 
 | ||
|  |         .. versionadded:: 2.3 | ||
|  |         """
 | ||
|  |         if self._cookies is None: | ||
|  |             raise TypeError( | ||
|  |                 "Cookies are disabled. Create a client with 'use_cookies=True'." | ||
|  |             ) | ||
|  | 
 | ||
|  |         return self._cookies.get((domain, path, key)) | ||
|  | 
 | ||
|  |     def set_cookie( | ||
|  |         self, | ||
|  |         key: str, | ||
|  |         value: str = "", | ||
|  |         *, | ||
|  |         domain: str = "localhost", | ||
|  |         origin_only: bool = True, | ||
|  |         path: str = "/", | ||
|  |         **kwargs: t.Any, | ||
|  |     ) -> None: | ||
|  |         """Set a cookie to be sent in subsequent requests.
 | ||
|  | 
 | ||
|  |         This is a convenience to skip making a test request to a route that would set | ||
|  |         the cookie. To test the cookie, make a test request to a route that uses the | ||
|  |         cookie value. | ||
|  | 
 | ||
|  |         The client uses ``domain``, ``origin_only``, and ``path`` to determine which | ||
|  |         cookies to send with a request. It does not use other cookie parameters that | ||
|  |         browsers use, since they're not applicable in tests. | ||
|  | 
 | ||
|  |         :param key: The key part of the cookie. | ||
|  |         :param value: The value part of the cookie. | ||
|  |         :param domain: Send this cookie with requests that match this domain. If | ||
|  |             ``origin_only`` is true, it must be an exact match, otherwise it may be a | ||
|  |             suffix match. | ||
|  |         :param origin_only: Whether the domain must be an exact match to the request. | ||
|  |         :param path: Send this cookie with requests that match this path either exactly | ||
|  |             or as a prefix. | ||
|  |         :param kwargs: Passed to :func:`.dump_cookie`. | ||
|  | 
 | ||
|  |         .. versionchanged:: 3.0 | ||
|  |             The parameter ``server_name`` is removed. The first parameter is | ||
|  |             ``key``. Use the ``domain`` and ``origin_only`` parameters instead. | ||
|  | 
 | ||
|  |         .. versionchanged:: 2.3 | ||
|  |             The ``origin_only`` parameter was added. | ||
|  | 
 | ||
|  |         .. versionchanged:: 2.3 | ||
|  |             The ``domain`` parameter defaults to ``localhost``. | ||
|  |         """
 | ||
|  |         if self._cookies is None: | ||
|  |             raise TypeError( | ||
|  |                 "Cookies are disabled. Create a client with 'use_cookies=True'." | ||
|  |             ) | ||
|  | 
 | ||
|  |         cookie = Cookie._from_response_header( | ||
|  |             domain, "/", dump_cookie(key, value, domain=domain, path=path, **kwargs) | ||
|  |         ) | ||
|  |         cookie.origin_only = origin_only | ||
|  | 
 | ||
|  |         if cookie._should_delete: | ||
|  |             self._cookies.pop(cookie._storage_key, None) | ||
|  |         else: | ||
|  |             self._cookies[cookie._storage_key] = cookie | ||
|  | 
 | ||
|  |     def delete_cookie( | ||
|  |         self, | ||
|  |         key: str, | ||
|  |         *, | ||
|  |         domain: str = "localhost", | ||
|  |         path: str = "/", | ||
|  |     ) -> None: | ||
|  |         """Delete a cookie if it exists. Cookies are uniquely identified by
 | ||
|  |         ``(domain, path, key)``. | ||
|  | 
 | ||
|  |         :param key: The decoded form of the key for the cookie. | ||
|  |         :param domain: The domain the cookie was set for. | ||
|  |         :param path: The path the cookie was set for. | ||
|  | 
 | ||
|  |         .. versionchanged:: 3.0 | ||
|  |             The ``server_name`` parameter is removed. The first parameter is | ||
|  |             ``key``. Use the ``domain`` parameter instead. | ||
|  | 
 | ||
|  |         .. versionchanged:: 3.0 | ||
|  |             The ``secure``, ``httponly`` and ``samesite`` parameters are removed. | ||
|  | 
 | ||
|  |         .. versionchanged:: 2.3 | ||
|  |             The ``domain`` parameter defaults to ``localhost``. | ||
|  |         """
 | ||
|  |         if self._cookies is None: | ||
|  |             raise TypeError( | ||
|  |                 "Cookies are disabled. Create a client with 'use_cookies=True'." | ||
|  |             ) | ||
|  | 
 | ||
|  |         self._cookies.pop((domain, path, key), None) | ||
|  | 
 | ||
|  |     def _add_cookies_to_wsgi(self, environ: WSGIEnvironment) -> None: | ||
|  |         """If cookies are enabled, set the ``Cookie`` header in the environ to the
 | ||
|  |         cookies that are applicable to the request host and path. | ||
|  | 
 | ||
|  |         :meta private: | ||
|  | 
 | ||
|  |         .. versionadded:: 2.3 | ||
|  |         """
 | ||
|  |         if self._cookies is None: | ||
|  |             return | ||
|  | 
 | ||
|  |         url = urlsplit(get_current_url(environ)) | ||
|  |         server_name = url.hostname or "localhost" | ||
|  |         value = "; ".join( | ||
|  |             c._to_request_header() | ||
|  |             for c in self._cookies.values() | ||
|  |             if c._matches_request(server_name, url.path) | ||
|  |         ) | ||
|  | 
 | ||
|  |         if value: | ||
|  |             environ["HTTP_COOKIE"] = value | ||
|  |         else: | ||
|  |             environ.pop("HTTP_COOKIE", None) | ||
|  | 
 | ||
|  |     def _update_cookies_from_response( | ||
|  |         self, server_name: str, path: str, headers: list[str] | ||
|  |     ) -> None: | ||
|  |         """If cookies are enabled, update the stored cookies from any ``Set-Cookie``
 | ||
|  |         headers in the response. | ||
|  | 
 | ||
|  |         :meta private: | ||
|  | 
 | ||
|  |         .. versionadded:: 2.3 | ||
|  |         """
 | ||
|  |         if self._cookies is None: | ||
|  |             return | ||
|  | 
 | ||
|  |         for header in headers: | ||
|  |             cookie = Cookie._from_response_header(server_name, path, header) | ||
|  | 
 | ||
|  |             if cookie._should_delete: | ||
|  |                 self._cookies.pop(cookie._storage_key, None) | ||
|  |             else: | ||
|  |                 self._cookies[cookie._storage_key] = cookie | ||
|  | 
 | ||
|  |     def run_wsgi_app( | ||
|  |         self, environ: WSGIEnvironment, buffered: bool = False | ||
|  |     ) -> tuple[t.Iterable[bytes], str, Headers]: | ||
|  |         """Runs the wrapped WSGI app with the given environment.
 | ||
|  | 
 | ||
|  |         :meta private: | ||
|  |         """
 | ||
|  |         self._add_cookies_to_wsgi(environ) | ||
|  |         rv = run_wsgi_app(self.application, environ, buffered=buffered) | ||
|  |         url = urlsplit(get_current_url(environ)) | ||
|  |         self._update_cookies_from_response( | ||
|  |             url.hostname or "localhost", url.path, rv[2].getlist("Set-Cookie") | ||
|  |         ) | ||
|  |         return rv | ||
|  | 
 | ||
|  |     def resolve_redirect( | ||
|  |         self, response: TestResponse, buffered: bool = False | ||
|  |     ) -> TestResponse: | ||
|  |         """Perform a new request to the location given by the redirect
 | ||
|  |         response to the previous request. | ||
|  | 
 | ||
|  |         :meta private: | ||
|  |         """
 | ||
|  |         scheme, netloc, path, qs, anchor = urlsplit(response.location) | ||
|  |         builder = EnvironBuilder.from_environ( | ||
|  |             response.request.environ, path=path, query_string=qs | ||
|  |         ) | ||
|  | 
 | ||
|  |         to_name_parts = netloc.split(":", 1)[0].split(".") | ||
|  |         from_name_parts = builder.server_name.split(".") | ||
|  | 
 | ||
|  |         if to_name_parts != [""]: | ||
|  |             # The new location has a host, use it for the base URL. | ||
|  |             builder.url_scheme = scheme | ||
|  |             builder.host = netloc | ||
|  |         else: | ||
|  |             # A local redirect with autocorrect_location_header=False | ||
|  |             # doesn't have a host, so use the request's host. | ||
|  |             to_name_parts = from_name_parts | ||
|  | 
 | ||
|  |         # Explain why a redirect to a different server name won't be followed. | ||
|  |         if to_name_parts != from_name_parts: | ||
|  |             if to_name_parts[-len(from_name_parts) :] == from_name_parts: | ||
|  |                 if not self.allow_subdomain_redirects: | ||
|  |                     raise RuntimeError("Following subdomain redirects is not enabled.") | ||
|  |             else: | ||
|  |                 raise RuntimeError("Following external redirects is not supported.") | ||
|  | 
 | ||
|  |         path_parts = path.split("/") | ||
|  |         root_parts = builder.script_root.split("/") | ||
|  | 
 | ||
|  |         if path_parts[: len(root_parts)] == root_parts: | ||
|  |             # Strip the script root from the path. | ||
|  |             builder.path = path[len(builder.script_root) :] | ||
|  |         else: | ||
|  |             # The new location is not under the script root, so use the | ||
|  |             # whole path and clear the previous root. | ||
|  |             builder.path = path | ||
|  |             builder.script_root = "" | ||
|  | 
 | ||
|  |         # Only 307 and 308 preserve all of the original request. | ||
|  |         if response.status_code not in {307, 308}: | ||
|  |             # HEAD is preserved, everything else becomes GET. | ||
|  |             if builder.method != "HEAD": | ||
|  |                 builder.method = "GET" | ||
|  | 
 | ||
|  |             # Clear the body and the headers that describe it. | ||
|  | 
 | ||
|  |             if builder.input_stream is not None: | ||
|  |                 builder.input_stream.close() | ||
|  |                 builder.input_stream = None | ||
|  | 
 | ||
|  |             builder.content_type = None | ||
|  |             builder.content_length = None | ||
|  |             builder.headers.pop("Transfer-Encoding", None) | ||
|  | 
 | ||
|  |         return self.open(builder, buffered=buffered) | ||
|  | 
 | ||
|  |     def open( | ||
|  |         self, | ||
|  |         *args: t.Any, | ||
|  |         buffered: bool = False, | ||
|  |         follow_redirects: bool = False, | ||
|  |         **kwargs: t.Any, | ||
|  |     ) -> TestResponse: | ||
|  |         """Generate an environ dict from the given arguments, make a
 | ||
|  |         request to the application using it, and return the response. | ||
|  | 
 | ||
|  |         :param args: Passed to :class:`EnvironBuilder` to create the | ||
|  |             environ for the request. If a single arg is passed, it can | ||
|  |             be an existing :class:`EnvironBuilder` or an environ dict. | ||
|  |         :param buffered: Convert the iterator returned by the app into | ||
|  |             a list. If the iterator has a ``close()`` method, it is | ||
|  |             called automatically. | ||
|  |         :param follow_redirects: Make additional requests to follow HTTP | ||
|  |             redirects until a non-redirect status is returned. | ||
|  |             :attr:`TestResponse.history` lists the intermediate | ||
|  |             responses. | ||
|  | 
 | ||
|  |         .. versionchanged:: 2.1 | ||
|  |             Removed the ``as_tuple`` parameter. | ||
|  | 
 | ||
|  |         .. versionchanged:: 2.0 | ||
|  |             The request input stream is closed when calling | ||
|  |             ``response.close()``. Input streams for redirects are | ||
|  |             automatically closed. | ||
|  | 
 | ||
|  |         .. versionchanged:: 0.5 | ||
|  |             If a dict is provided as file in the dict for the ``data`` | ||
|  |             parameter the content type has to be called ``content_type`` | ||
|  |             instead of ``mimetype``. This change was made for | ||
|  |             consistency with :class:`werkzeug.FileWrapper`. | ||
|  | 
 | ||
|  |         .. versionchanged:: 0.5 | ||
|  |             Added the ``follow_redirects`` parameter. | ||
|  |         """
 | ||
|  |         request: Request | None = None | ||
|  | 
 | ||
|  |         if not kwargs and len(args) == 1: | ||
|  |             arg = args[0] | ||
|  | 
 | ||
|  |             if isinstance(arg, EnvironBuilder): | ||
|  |                 request = arg.get_request() | ||
|  |             elif isinstance(arg, dict): | ||
|  |                 request = EnvironBuilder.from_environ(arg).get_request() | ||
|  |             elif isinstance(arg, Request): | ||
|  |                 request = arg | ||
|  | 
 | ||
|  |         if request is None: | ||
|  |             builder = EnvironBuilder(*args, **kwargs) | ||
|  | 
 | ||
|  |             try: | ||
|  |                 request = builder.get_request() | ||
|  |             finally: | ||
|  |                 builder.close() | ||
|  | 
 | ||
|  |         response_parts = self.run_wsgi_app(request.environ, buffered=buffered) | ||
|  |         response = self.response_wrapper(*response_parts, request=request) | ||
|  | 
 | ||
|  |         redirects = set() | ||
|  |         history: list[TestResponse] = [] | ||
|  | 
 | ||
|  |         if not follow_redirects: | ||
|  |             return response | ||
|  | 
 | ||
|  |         while response.status_code in { | ||
|  |             301, | ||
|  |             302, | ||
|  |             303, | ||
|  |             305, | ||
|  |             307, | ||
|  |             308, | ||
|  |         }: | ||
|  |             # Exhaust intermediate response bodies to ensure middleware | ||
|  |             # that returns an iterator runs any cleanup code. | ||
|  |             if not buffered: | ||
|  |                 response.make_sequence() | ||
|  |                 response.close() | ||
|  | 
 | ||
|  |             new_redirect_entry = (response.location, response.status_code) | ||
|  | 
 | ||
|  |             if new_redirect_entry in redirects: | ||
|  |                 raise ClientRedirectError( | ||
|  |                     f"Loop detected: A {response.status_code} redirect" | ||
|  |                     f" to {response.location} was already made." | ||
|  |                 ) | ||
|  | 
 | ||
|  |             redirects.add(new_redirect_entry) | ||
|  |             response.history = tuple(history) | ||
|  |             history.append(response) | ||
|  |             response = self.resolve_redirect(response, buffered=buffered) | ||
|  |         else: | ||
|  |             # This is the final request after redirects. | ||
|  |             response.history = tuple(history) | ||
|  |             # Close the input stream when closing the response, in case | ||
|  |             # the input is an open temporary file. | ||
|  |             response.call_on_close(request.input_stream.close) | ||
|  |             return response | ||
|  | 
 | ||
|  |     def get(self, *args: t.Any, **kw: t.Any) -> TestResponse: | ||
|  |         """Call :meth:`open` with ``method`` set to ``GET``.""" | ||
|  |         kw["method"] = "GET" | ||
|  |         return self.open(*args, **kw) | ||
|  | 
 | ||
|  |     def post(self, *args: t.Any, **kw: t.Any) -> TestResponse: | ||
|  |         """Call :meth:`open` with ``method`` set to ``POST``.""" | ||
|  |         kw["method"] = "POST" | ||
|  |         return self.open(*args, **kw) | ||
|  | 
 | ||
|  |     def put(self, *args: t.Any, **kw: t.Any) -> TestResponse: | ||
|  |         """Call :meth:`open` with ``method`` set to ``PUT``.""" | ||
|  |         kw["method"] = "PUT" | ||
|  |         return self.open(*args, **kw) | ||
|  | 
 | ||
|  |     def delete(self, *args: t.Any, **kw: t.Any) -> TestResponse: | ||
|  |         """Call :meth:`open` with ``method`` set to ``DELETE``.""" | ||
|  |         kw["method"] = "DELETE" | ||
|  |         return self.open(*args, **kw) | ||
|  | 
 | ||
|  |     def patch(self, *args: t.Any, **kw: t.Any) -> TestResponse: | ||
|  |         """Call :meth:`open` with ``method`` set to ``PATCH``.""" | ||
|  |         kw["method"] = "PATCH" | ||
|  |         return self.open(*args, **kw) | ||
|  | 
 | ||
|  |     def options(self, *args: t.Any, **kw: t.Any) -> TestResponse: | ||
|  |         """Call :meth:`open` with ``method`` set to ``OPTIONS``.""" | ||
|  |         kw["method"] = "OPTIONS" | ||
|  |         return self.open(*args, **kw) | ||
|  | 
 | ||
|  |     def head(self, *args: t.Any, **kw: t.Any) -> TestResponse: | ||
|  |         """Call :meth:`open` with ``method`` set to ``HEAD``.""" | ||
|  |         kw["method"] = "HEAD" | ||
|  |         return self.open(*args, **kw) | ||
|  | 
 | ||
|  |     def trace(self, *args: t.Any, **kw: t.Any) -> TestResponse: | ||
|  |         """Call :meth:`open` with ``method`` set to ``TRACE``.""" | ||
|  |         kw["method"] = "TRACE" | ||
|  |         return self.open(*args, **kw) | ||
|  | 
 | ||
|  |     def __repr__(self) -> str: | ||
|  |         return f"<{type(self).__name__} {self.application!r}>" | ||
|  | 
 | ||
|  | 
 | ||
|  | def create_environ(*args: t.Any, **kwargs: t.Any) -> WSGIEnvironment: | ||
|  |     """Create a new WSGI environ dict based on the values passed.  The first
 | ||
|  |     parameter should be the path of the request which defaults to '/'.  The | ||
|  |     second one can either be an absolute path (in that case the host is | ||
|  |     localhost:80) or a full path to the request with scheme, netloc port and | ||
|  |     the path to the script. | ||
|  | 
 | ||
|  |     This accepts the same arguments as the :class:`EnvironBuilder` | ||
|  |     constructor. | ||
|  | 
 | ||
|  |     .. versionchanged:: 0.5 | ||
|  |        This function is now a thin wrapper over :class:`EnvironBuilder` which | ||
|  |        was added in 0.5.  The `headers`, `environ_base`, `environ_overrides` | ||
|  |        and `charset` parameters were added. | ||
|  |     """
 | ||
|  |     builder = EnvironBuilder(*args, **kwargs) | ||
|  | 
 | ||
|  |     try: | ||
|  |         return builder.get_environ() | ||
|  |     finally: | ||
|  |         builder.close() | ||
|  | 
 | ||
|  | 
 | ||
|  | def run_wsgi_app( | ||
|  |     app: WSGIApplication, environ: WSGIEnvironment, buffered: bool = False | ||
|  | ) -> tuple[t.Iterable[bytes], str, Headers]: | ||
|  |     """Return a tuple in the form (app_iter, status, headers) of the
 | ||
|  |     application output.  This works best if you pass it an application that | ||
|  |     returns an iterator all the time. | ||
|  | 
 | ||
|  |     Sometimes applications may use the `write()` callable returned | ||
|  |     by the `start_response` function.  This tries to resolve such edge | ||
|  |     cases automatically.  But if you don't get the expected output you | ||
|  |     should set `buffered` to `True` which enforces buffering. | ||
|  | 
 | ||
|  |     If passed an invalid WSGI application the behavior of this function is | ||
|  |     undefined.  Never pass non-conforming WSGI applications to this function. | ||
|  | 
 | ||
|  |     :param app: the application to execute. | ||
|  |     :param buffered: set to `True` to enforce buffering. | ||
|  |     :return: tuple in the form ``(app_iter, status, headers)`` | ||
|  |     """
 | ||
|  |     # Copy environ to ensure any mutations by the app (ProxyFix, for | ||
|  |     # example) don't affect subsequent requests (such as redirects). | ||
|  |     environ = _get_environ(environ).copy() | ||
|  |     status: str | ||
|  |     response: tuple[str, list[tuple[str, str]]] | None = None | ||
|  |     buffer: list[bytes] = [] | ||
|  | 
 | ||
|  |     def start_response(status, headers, exc_info=None):  # type: ignore | ||
|  |         nonlocal response | ||
|  | 
 | ||
|  |         if exc_info: | ||
|  |             try: | ||
|  |                 raise exc_info[1].with_traceback(exc_info[2]) | ||
|  |             finally: | ||
|  |                 exc_info = None | ||
|  | 
 | ||
|  |         response = (status, headers) | ||
|  |         return buffer.append | ||
|  | 
 | ||
|  |     app_rv = app(environ, start_response) | ||
|  |     close_func = getattr(app_rv, "close", None) | ||
|  |     app_iter: t.Iterable[bytes] = iter(app_rv) | ||
|  | 
 | ||
|  |     # when buffering we emit the close call early and convert the | ||
|  |     # application iterator into a regular list | ||
|  |     if buffered: | ||
|  |         try: | ||
|  |             app_iter = list(app_iter) | ||
|  |         finally: | ||
|  |             if close_func is not None: | ||
|  |                 close_func() | ||
|  | 
 | ||
|  |     # otherwise we iterate the application iter until we have a response, chain | ||
|  |     # the already received data with the already collected data and wrap it in | ||
|  |     # a new `ClosingIterator` if we need to restore a `close` callable from the | ||
|  |     # original return value. | ||
|  |     else: | ||
|  |         for item in app_iter: | ||
|  |             buffer.append(item) | ||
|  | 
 | ||
|  |             if response is not None: | ||
|  |                 break | ||
|  | 
 | ||
|  |         if buffer: | ||
|  |             app_iter = chain(buffer, app_iter) | ||
|  | 
 | ||
|  |         if close_func is not None and app_iter is not app_rv: | ||
|  |             app_iter = ClosingIterator(app_iter, close_func) | ||
|  | 
 | ||
|  |     status, headers = response  # type: ignore | ||
|  |     return app_iter, status, Headers(headers) | ||
|  | 
 | ||
|  | 
 | ||
|  | class TestResponse(Response): | ||
|  |     """:class:`~werkzeug.wrappers.Response` subclass that provides extra
 | ||
|  |     information about requests made with the test :class:`Client`. | ||
|  | 
 | ||
|  |     Test client requests will always return an instance of this class. | ||
|  |     If a custom response class is passed to the client, it is | ||
|  |     subclassed along with this to support test information. | ||
|  | 
 | ||
|  |     If the test request included large files, or if the application is | ||
|  |     serving a file, call :meth:`close` to close any open files and | ||
|  |     prevent Python showing a ``ResourceWarning``. | ||
|  | 
 | ||
|  |     .. versionchanged:: 2.2 | ||
|  |         Set the ``default_mimetype`` to None to prevent a mimetype being | ||
|  |         assumed if missing. | ||
|  | 
 | ||
|  |     .. versionchanged:: 2.1 | ||
|  |         Response instances cannot be treated as tuples. | ||
|  | 
 | ||
|  |     .. versionadded:: 2.0 | ||
|  |         Test client methods always return instances of this class. | ||
|  |     """
 | ||
|  | 
 | ||
|  |     default_mimetype = None | ||
|  |     # Don't assume a mimetype, instead use whatever the response provides | ||
|  | 
 | ||
|  |     request: Request | ||
|  |     """A request object with the environ used to make the request that
 | ||
|  |     resulted in this response. | ||
|  |     """
 | ||
|  | 
 | ||
|  |     history: tuple[TestResponse, ...] | ||
|  |     """A list of intermediate responses. Populated when the test request
 | ||
|  |     is made with ``follow_redirects`` enabled. | ||
|  |     """
 | ||
|  | 
 | ||
|  |     # Tell Pytest to ignore this, it's not a test class. | ||
|  |     __test__ = False | ||
|  | 
 | ||
|  |     def __init__( | ||
|  |         self, | ||
|  |         response: t.Iterable[bytes], | ||
|  |         status: str, | ||
|  |         headers: Headers, | ||
|  |         request: Request, | ||
|  |         history: tuple[TestResponse] = (),  # type: ignore | ||
|  |         **kwargs: t.Any, | ||
|  |     ) -> None: | ||
|  |         super().__init__(response, status, headers, **kwargs) | ||
|  |         self.request = request | ||
|  |         self.history = history | ||
|  |         self._compat_tuple = response, status, headers | ||
|  | 
 | ||
|  |     @cached_property | ||
|  |     def text(self) -> str: | ||
|  |         """The response data as text. A shortcut for
 | ||
|  |         ``response.get_data(as_text=True)``. | ||
|  | 
 | ||
|  |         .. versionadded:: 2.1 | ||
|  |         """
 | ||
|  |         return self.get_data(as_text=True) | ||
|  | 
 | ||
|  | 
 | ||
|  | @dataclasses.dataclass | ||
|  | class Cookie: | ||
|  |     """A cookie key, value, and parameters.
 | ||
|  | 
 | ||
|  |     The class itself is not a public API. Its attributes are documented for inspection | ||
|  |     with :meth:`.Client.get_cookie` only. | ||
|  | 
 | ||
|  |     .. versionadded:: 2.3 | ||
|  |     """
 | ||
|  | 
 | ||
|  |     key: str | ||
|  |     """The cookie key, encoded as a client would see it.""" | ||
|  | 
 | ||
|  |     value: str | ||
|  |     """The cookie key, encoded as a client would see it.""" | ||
|  | 
 | ||
|  |     decoded_key: str | ||
|  |     """The cookie key, decoded as the application would set and see it.""" | ||
|  | 
 | ||
|  |     decoded_value: str | ||
|  |     """The cookie value, decoded as the application would set and see it.""" | ||
|  | 
 | ||
|  |     expires: datetime | None | ||
|  |     """The time at which the cookie is no longer valid.""" | ||
|  | 
 | ||
|  |     max_age: int | None | ||
|  |     """The number of seconds from when the cookie was set at which it is
 | ||
|  |     no longer valid. | ||
|  |     """
 | ||
|  | 
 | ||
|  |     domain: str | ||
|  |     """The domain that the cookie was set for, or the request domain if not set.""" | ||
|  | 
 | ||
|  |     origin_only: bool | ||
|  |     """Whether the cookie will be sent for exact domain matches only. This is ``True``
 | ||
|  |     if the ``Domain`` parameter was not present. | ||
|  |     """
 | ||
|  | 
 | ||
|  |     path: str | ||
|  |     """The path that the cookie was set for.""" | ||
|  | 
 | ||
|  |     secure: bool | None | ||
|  |     """The ``Secure`` parameter.""" | ||
|  | 
 | ||
|  |     http_only: bool | None | ||
|  |     """The ``HttpOnly`` parameter.""" | ||
|  | 
 | ||
|  |     same_site: str | None | ||
|  |     """The ``SameSite`` parameter.""" | ||
|  | 
 | ||
|  |     def _matches_request(self, server_name: str, path: str) -> bool: | ||
|  |         return ( | ||
|  |             server_name == self.domain | ||
|  |             or ( | ||
|  |                 not self.origin_only | ||
|  |                 and server_name.endswith(self.domain) | ||
|  |                 and server_name[: -len(self.domain)].endswith(".") | ||
|  |             ) | ||
|  |         ) and ( | ||
|  |             path == self.path | ||
|  |             or ( | ||
|  |                 path.startswith(self.path) | ||
|  |                 and path[len(self.path) - self.path.endswith("/") :].startswith("/") | ||
|  |             ) | ||
|  |         ) | ||
|  | 
 | ||
|  |     def _to_request_header(self) -> str: | ||
|  |         return f"{self.key}={self.value}" | ||
|  | 
 | ||
|  |     @classmethod | ||
|  |     def _from_response_header(cls, server_name: str, path: str, header: str) -> te.Self: | ||
|  |         header, _, parameters_str = header.partition(";") | ||
|  |         key, _, value = header.partition("=") | ||
|  |         decoded_key, decoded_value = next(parse_cookie(header).items())  # type: ignore[call-overload] | ||
|  |         params = {} | ||
|  | 
 | ||
|  |         for item in parameters_str.split(";"): | ||
|  |             k, sep, v = item.partition("=") | ||
|  |             params[k.strip().lower()] = v.strip() if sep else None | ||
|  | 
 | ||
|  |         return cls( | ||
|  |             key=key.strip(), | ||
|  |             value=value.strip(), | ||
|  |             decoded_key=decoded_key, | ||
|  |             decoded_value=decoded_value, | ||
|  |             expires=parse_date(params.get("expires")), | ||
|  |             max_age=int(params["max-age"] or 0) if "max-age" in params else None, | ||
|  |             domain=params.get("domain") or server_name, | ||
|  |             origin_only="domain" not in params, | ||
|  |             path=params.get("path") or path.rpartition("/")[0] or "/", | ||
|  |             secure="secure" in params, | ||
|  |             http_only="httponly" in params, | ||
|  |             same_site=params.get("samesite"), | ||
|  |         ) | ||
|  | 
 | ||
|  |     @property | ||
|  |     def _storage_key(self) -> tuple[str, str, str]: | ||
|  |         return self.domain, self.path, self.decoded_key | ||
|  | 
 | ||
|  |     @property | ||
|  |     def _should_delete(self) -> bool: | ||
|  |         return self.max_age == 0 or ( | ||
|  |             self.expires is not None and self.expires.timestamp() == 0 | ||
|  |         ) |