codekingpro/portable-devtools
114k
1from __future__ import annotations2 3import json4import typing as t5from http import HTTPStatus6from urllib.parse import urljoin7 8from .._internal import _get_environ9from ..datastructures import Headers10from ..http import generate_etag11from ..http import http_date12from ..http import is_resource_modified13from ..http import parse_etags14from ..http import parse_range_header15from ..http import remove_entity_headers16from ..sansio.response import Response as _SansIOResponse17from ..urls import iri_to_uri18from ..utils import cached_property19from ..wsgi import _RangeWrapper20from ..wsgi import ClosingIterator21from ..wsgi import get_current_url22 23if t.TYPE_CHECKING:24 from _typeshed.wsgi import StartResponse25 from _typeshed.wsgi import WSGIApplication26 from _typeshed.wsgi import WSGIEnvironment27 28 from .request import Request29 30 31def _iter_encoded(iterable: t.Iterable[str | bytes]) -> t.Iterator[bytes]:32 for item in iterable:33 if isinstance(item, str):34 yield item.encode()35 else:36 yield item37 38 39class Response(_SansIOResponse):40 """Represents an outgoing WSGI HTTP response with body, status, and41 headers. Has properties and methods for using the functionality42 defined by various HTTP specs.43 44 The response body is flexible to support different use cases. The45 simple form is passing bytes, or a string which will be encoded as46 UTF-8. Passing an iterable of bytes or strings makes this a47 streaming response. A generator is particularly useful for building48 a CSV file in memory or using SSE (Server Sent Events). A file-like49 object is also iterable, although the50 :func:`~werkzeug.utils.send_file` helper should be used in that51 case.52 53 The response object is itself a WSGI application callable. When54 called (:meth:`__call__`) with ``environ`` and ``start_response``,55 it will pass its status and headers to ``start_response`` then56 return its body as an iterable.57 58 .. code-block:: python59 60 from werkzeug.wrappers.response import Response61 62 def index():63 return Response("Hello, World!")64 65 def application(environ, start_response):66 path = environ.get("PATH_INFO") or "/"67 68 if path == "/":69 response = index()70 else:71 response = Response("Not Found", status=404)72 73 return response(environ, start_response)74 75 :param response: The data for the body of the response. A string or76 bytes, or tuple or list of strings or bytes, for a fixed-length77 response, or any other iterable of strings or bytes for a78 streaming response. Defaults to an empty body.79 :param status: The status code for the response. Either an int, in80 which case the default status message is added, or a string in81 the form ``{code} {message}``, like ``404 Not Found``. Defaults82 to 200.83 :param headers: A :class:`~werkzeug.datastructures.Headers` object,84 or a list of ``(key, value)`` tuples that will be converted to a85 ``Headers`` object.86 :param mimetype: The mime type (content type without charset or87 other parameters) of the response. If the value starts with88 ``text/`` (or matches some other special cases), the charset89 will be added to create the ``content_type``.90 :param content_type: The full content type of the response.91 Overrides building the value from ``mimetype``.92 :param direct_passthrough: Pass the response body directly through93 as the WSGI iterable. This can be used when the body is a binary94 file or other iterator of bytes, to skip some unnecessary95 checks. Use :func:`~werkzeug.utils.send_file` instead of setting96 this manually.97 98 .. versionchanged:: 2.199 Old ``BaseResponse`` and mixin classes were removed.100 101 .. versionchanged:: 2.0102 Combine ``BaseResponse`` and mixins into a single ``Response``103 class.104 105 .. versionchanged:: 0.5106 The ``direct_passthrough`` parameter was added.107 """108 109 #: if set to `False` accessing properties on the response object will110 #: not try to consume the response iterator and convert it into a list.111 #:112 #: .. versionadded:: 0.6.2113 #:114 #: That attribute was previously called `implicit_seqence_conversion`.115 #: (Notice the typo). If you did use this feature, you have to adapt116 #: your code to the name change.117 implicit_sequence_conversion = True118 119 #: If a redirect ``Location`` header is a relative URL, make it an120 #: absolute URL, including scheme and domain.121 #:122 #: .. versionchanged:: 2.1123 #: This is disabled by default, so responses will send relative124 #: redirects.125 #:126 #: .. versionadded:: 0.8127 autocorrect_location_header = False128 129 #: Should this response object automatically set the content-length130 #: header if possible? This is true by default.131 #:132 #: .. versionadded:: 0.8133 automatically_set_content_length = True134 135 #: The response body to send as the WSGI iterable. A list of strings136 #: or bytes represents a fixed-length response, any other iterable137 #: is a streaming response. Strings are encoded to bytes as UTF-8.138 #:139 #: Do not set to a plain string or bytes, that will cause sending140 #: the response to be very inefficient as it will iterate one byte141 #: at a time.142 response: t.Iterable[str] | t.Iterable[bytes]143 144 def __init__(145 self,146 response: t.Iterable[bytes] | bytes | t.Iterable[str] | str | None = None,147 status: int | str | HTTPStatus | None = None,148 headers: t.Mapping[str, str | t.Iterable[str]]149 | t.Iterable[tuple[str, str]]150 | None = None,151 mimetype: str | None = None,152 content_type: str | None = None,153 direct_passthrough: bool = False,154 ) -> None:155 super().__init__(156 status=status,157 headers=headers,158 mimetype=mimetype,159 content_type=content_type,160 )161 162 #: Pass the response body directly through as the WSGI iterable.163 #: This can be used when the body is a binary file or other164 #: iterator of bytes, to skip some unnecessary checks. Use165 #: :func:`~werkzeug.utils.send_file` instead of setting this166 #: manually.167 self.direct_passthrough = direct_passthrough168 self._on_close: list[t.Callable[[], t.Any]] = []169 170 # we set the response after the headers so that if a class changes171 # the charset attribute, the data is set in the correct charset.172 if response is None:173 self.response = []174 elif isinstance(response, (str, bytes, bytearray)):175 self.set_data(response)176 else:177 self.response = response178 179 def call_on_close(self, func: t.Callable[[], t.Any]) -> t.Callable[[], t.Any]:180 """Adds a function to the internal list of functions that should181 be called as part of closing down the response. Since 0.7 this182 function also returns the function that was passed so that this183 can be used as a decorator.184 185 .. versionadded:: 0.6186 """187 self._on_close.append(func)188 return func189 190 def __repr__(self) -> str:191 if self.is_sequence:192 body_info = f"{sum(map(len, self.iter_encoded()))} bytes"193 else:194 body_info = "streamed" if self.is_streamed else "likely-streamed"195 return f"<{type(self).__name__} {body_info} [{self.status}]>"196 197 @classmethod198 def force_type(199 cls, response: Response, environ: WSGIEnvironment | None = None200 ) -> Response:201 """Enforce that the WSGI response is a response object of the current202 type. Werkzeug will use the :class:`Response` internally in many203 situations like the exceptions. If you call :meth:`get_response` on an204 exception you will get back a regular :class:`Response` object, even205 if you are using a custom subclass.206 207 This method can enforce a given response type, and it will also208 convert arbitrary WSGI callables into response objects if an environ209 is provided::210 211 # convert a Werkzeug response object into an instance of the212 # MyResponseClass subclass.213 response = MyResponseClass.force_type(response)214 215 # convert any WSGI application into a response object216 response = MyResponseClass.force_type(response, environ)217 218 This is especially useful if you want to post-process responses in219 the main dispatcher and use functionality provided by your subclass.220 221 Keep in mind that this will modify response objects in place if222 possible!223 224 :param response: a response object or wsgi application.225 :param environ: a WSGI environment object.226 :return: a response object.227 """228 if not isinstance(response, Response):229 if environ is None:230 raise TypeError(231 "cannot convert WSGI application into response"232 " objects without an environ"233 )234 235 from ..test import run_wsgi_app236 237 response = Response(*run_wsgi_app(response, environ))238 239 response.__class__ = cls240 return response241 242 @classmethod243 def from_app(244 cls, app: WSGIApplication, environ: WSGIEnvironment, buffered: bool = False245 ) -> Response:246 """Create a new response object from an application output. This247 works best if you pass it an application that returns a generator all248 the time. Sometimes applications may use the `write()` callable249 returned by the `start_response` function. This tries to resolve such250 edge cases automatically. But if you don't get the expected output251 you should set `buffered` to `True` which enforces buffering.252 253 :param app: the WSGI application to execute.254 :param environ: the WSGI environment to execute against.255 :param buffered: set to `True` to enforce buffering.256 :return: a response object.257 """258 from ..test import run_wsgi_app259 260 return cls(*run_wsgi_app(app, environ, buffered))261 262 @t.overload263 def get_data(self, as_text: t.Literal[False] = False) -> bytes: ...264 265 @t.overload266 def get_data(self, as_text: t.Literal[True]) -> str: ...267 268 def get_data(self, as_text: bool = False) -> bytes | str:269 """The string representation of the response body. Whenever you call270 this property the response iterable is encoded and flattened. This271 can lead to unwanted behavior if you stream big data.272 273 This behavior can be disabled by setting274 :attr:`implicit_sequence_conversion` to `False`.275 276 If `as_text` is set to `True` the return value will be a decoded277 string.278 279 .. versionadded:: 0.9280 """281 self._ensure_sequence()282 rv = b"".join(self.iter_encoded())283 284 if as_text:285 return rv.decode()286 287 return rv288 289 def set_data(self, value: bytes | str) -> None:290 """Sets a new string as response. The value must be a string or291 bytes. If a string is set it's encoded to the charset of the292 response (utf-8 by default).293 294 .. versionadded:: 0.9295 """296 if isinstance(value, str):297 value = value.encode()298 self.response = [value]299 if self.automatically_set_content_length:300 self.headers["Content-Length"] = str(len(value))301 302 data = property(303 get_data,304 set_data,305 doc="A descriptor that calls :meth:`get_data` and :meth:`set_data`.",306 )307 308 def calculate_content_length(self) -> int | None:309 """Returns the content length if available or `None` otherwise."""310 try:311 self._ensure_sequence()312 except RuntimeError:313 return None314 return sum(len(x) for x in self.iter_encoded())315 316 def _ensure_sequence(self, mutable: bool = False) -> None:317 """This method can be called by methods that need a sequence. If318 `mutable` is true, it will also ensure that the response sequence319 is a standard Python list.320 321 .. versionadded:: 0.6322 """323 if self.is_sequence:324 # if we need a mutable object, we ensure it's a list.325 if mutable and not isinstance(self.response, list):326 self.response = list(self.response) # type: ignore327 return328 if self.direct_passthrough:329 raise RuntimeError(330 "Attempted implicit sequence conversion but the"331 " response object is in direct passthrough mode."332 )333 if not self.implicit_sequence_conversion:334 raise RuntimeError(335 "The response object required the iterable to be a"336 " sequence, but the implicit conversion was disabled."337 " Call make_sequence() yourself."338 )339 self.make_sequence()340 341 def make_sequence(self) -> None:342 """Converts the response iterator in a list. By default this happens343 automatically if required. If `implicit_sequence_conversion` is344 disabled, this method is not automatically called and some properties345 might raise exceptions. This also encodes all the items.346 347 .. versionadded:: 0.6348 """349 if not self.is_sequence:350 # if we consume an iterable we have to ensure that the close351 # method of the iterable is called if available when we tear352 # down the response353 close = getattr(self.response, "close", None)354 self.response = list(self.iter_encoded())355 if close is not None:356 self.call_on_close(close)357 358 def iter_encoded(self) -> t.Iterator[bytes]:359 """Iter the response encoded with the encoding of the response.360 If the response object is invoked as WSGI application the return361 value of this method is used as application iterator unless362 :attr:`direct_passthrough` was activated.363 """364 # Encode in a separate function so that self.response is fetched365 # early. This allows us to wrap the response with the return366 # value from get_app_iter or iter_encoded.367 return _iter_encoded(self.response)368 369 @property370 def is_streamed(self) -> bool:371 """If the response is streamed (the response is not an iterable with372 a length information) this property is `True`. In this case streamed373 means that there is no information about the number of iterations.374 This is usually `True` if a generator is passed to the response object.375 376 This is useful for checking before applying some sort of post377 filtering that should not take place for streamed responses.378 """379 try:380 len(self.response) # type: ignore381 except (TypeError, AttributeError):382 return True383 return False384 385 @property386 def is_sequence(self) -> bool:387 """If the iterator is buffered, this property will be `True`. A388 response object will consider an iterator to be buffered if the389 response attribute is a list or tuple.390 391 .. versionadded:: 0.6392 """393 return isinstance(self.response, (tuple, list))394 395 def close(self) -> None:396 """Close the wrapped response if possible. You can also use the object397 in a with statement which will automatically close it.398 399 .. versionadded:: 0.9400 Can now be used in a with statement.401 """402 if hasattr(self.response, "close"):403 self.response.close()404 for func in self._on_close:405 func()406 407 def __enter__(self) -> Response:408 return self409 410 def __exit__(self, exc_type, exc_value, tb): # type: ignore411 self.close()412 413 def freeze(self) -> None:414 """Make the response object ready to be pickled. Does the415 following:416 417 * Buffer the response into a list, ignoring418 :attr:`implicity_sequence_conversion` and419 :attr:`direct_passthrough`.420 * Set the ``Content-Length`` header.421 * Generate an ``ETag`` header if one is not already set.422 423 .. versionchanged:: 2.1424 Removed the ``no_etag`` parameter.425 426 .. versionchanged:: 2.0427 An ``ETag`` header is always added.428 429 .. versionchanged:: 0.6430 The ``Content-Length`` header is set.431 """432 # Always freeze the encoded response body, ignore433 # implicit_sequence_conversion and direct_passthrough.434 self.response = list(self.iter_encoded())435 self.headers["Content-Length"] = str(sum(map(len, self.response)))436 self.add_etag()437 438 def get_wsgi_headers(self, environ: WSGIEnvironment) -> Headers:439 """This is automatically called right before the response is started440 and returns headers modified for the given environment. It returns a441 copy of the headers from the response with some modifications applied442 if necessary.443 444 For example the location header (if present) is joined with the root445 URL of the environment. Also the content length is automatically set446 to zero here for certain status codes.447 448 .. versionchanged:: 0.6449 Previously that function was called `fix_headers` and modified450 the response object in place. Also since 0.6, IRIs in location451 and content-location headers are handled properly.452 453 Also starting with 0.6, Werkzeug will attempt to set the content454 length if it is able to figure it out on its own. This is the455 case if all the strings in the response iterable are already456 encoded and the iterable is buffered.457 458 :param environ: the WSGI environment of the request.459 :return: returns a new :class:`~werkzeug.datastructures.Headers`460 object.461 """462 headers = Headers(self.headers)463 location: str | None = None464 content_location: str | None = None465 content_length: str | int | None = None466 status = self.status_code467 468 # iterate over the headers to find all values in one go. Because469 # get_wsgi_headers is used each response that gives us a tiny470 # speedup.471 for key, value in headers:472 ikey = key.lower()473 if ikey == "location":474 location = value475 elif ikey == "content-location":476 content_location = value477 elif ikey == "content-length":478 content_length = value479 480 if location is not None:481 location = iri_to_uri(location)482 483 if self.autocorrect_location_header:484 # Make the location header an absolute URL.485 current_url = get_current_url(environ, strip_querystring=True)486 current_url = iri_to_uri(current_url)487 location = urljoin(current_url, location)488 489 headers["Location"] = location490 491 # make sure the content location is a URL492 if content_location is not None:493 headers["Content-Location"] = iri_to_uri(content_location)494 495 if 100 <= status < 200 or status == 204:496 # Per section 3.3.2 of RFC 7230, "a server MUST NOT send a497 # Content-Length header field in any response with a status498 # code of 1xx (Informational) or 204 (No Content)."499 headers.remove("Content-Length")500 elif status == 304:501 remove_entity_headers(headers)502 503 # if we can determine the content length automatically, we504 # should try to do that. But only if this does not involve505 # flattening the iterator or encoding of strings in the506 # response. We however should not do that if we have a 304507 # response.508 if (509 self.automatically_set_content_length510 and self.is_sequence511 and content_length is None512 and status not in (204, 304)513 and not (100 <= status < 200)514 ):515 content_length = sum(len(x) for x in self.iter_encoded())516 headers["Content-Length"] = str(content_length)517 518 return headers519 520 def get_app_iter(self, environ: WSGIEnvironment) -> t.Iterable[bytes]:521 """Returns the application iterator for the given environ. Depending522 on the request method and the current status code the return value523 might be an empty response rather than the one from the response.524 525 If the request method is `HEAD` or the status code is in a range526 where the HTTP specification requires an empty response, an empty527 iterable is returned.528 529 .. versionadded:: 0.6530 531 :param environ: the WSGI environment of the request.532 :return: a response iterable.533 """534 status = self.status_code535 if (536 environ["REQUEST_METHOD"] == "HEAD"537 or 100 <= status < 200538 or status in (204, 304)539 ):540 iterable: t.Iterable[bytes] = ()541 elif self.direct_passthrough:542 return self.response # type: ignore543 else:544 iterable = self.iter_encoded()545 return ClosingIterator(iterable, self.close)546 547 def get_wsgi_response(548 self, environ: WSGIEnvironment549 ) -> tuple[t.Iterable[bytes], str, list[tuple[str, str]]]:550 """Returns the final WSGI response as tuple. The first item in551 the tuple is the application iterator, the second the status and552 the third the list of headers. The response returned is created553 specially for the given environment. For example if the request554 method in the WSGI environment is ``'HEAD'`` the response will555 be empty and only the headers and status code will be present.556 557 .. versionadded:: 0.6558 559 :param environ: the WSGI environment of the request.560 :return: an ``(app_iter, status, headers)`` tuple.561 """562 headers = self.get_wsgi_headers(environ)563 app_iter = self.get_app_iter(environ)564 return app_iter, self.status, headers.to_wsgi_list()565 566 def __call__(567 self, environ: WSGIEnvironment, start_response: StartResponse568 ) -> t.Iterable[bytes]:569 """Process this response as WSGI application.570 571 :param environ: the WSGI environment.572 :param start_response: the response callable provided by the WSGI573 server.574 :return: an application iterator575 """576 app_iter, status, headers = self.get_wsgi_response(environ)577 start_response(status, headers)578 return app_iter579 580 # JSON581 582 #: A module or other object that has ``dumps`` and ``loads``583 #: functions that match the API of the built-in :mod:`json` module.584 json_module = json585 586 @property587 def json(self) -> t.Any | None:588 """The parsed JSON data if :attr:`mimetype` indicates JSON589 (:mimetype:`application/json`, see :attr:`is_json`).590 591 Calls :meth:`get_json` with default arguments.592 """593 return self.get_json()594 595 @t.overload596 def get_json(self, force: bool = ..., silent: t.Literal[False] = ...) -> t.Any: ...597 598 @t.overload599 def get_json(self, force: bool = ..., silent: bool = ...) -> t.Any | None: ...600 601 def get_json(self, force: bool = False, silent: bool = False) -> t.Any | None:602 """Parse :attr:`data` as JSON. Useful during testing.603 604 If the mimetype does not indicate JSON605 (:mimetype:`application/json`, see :attr:`is_json`), this606 returns ``None``.607 608 Unlike :meth:`Request.get_json`, the result is not cached.609 610 :param force: Ignore the mimetype and always try to parse JSON.611 :param silent: Silence parsing errors and return ``None``612 instead.613 """614 if not (force or self.is_json):615 return None616 617 data = self.get_data()618 619 try:620 return self.json_module.loads(data)621 except ValueError:622 if not silent:623 raise624 625 return None626 627 # Stream628 629 @cached_property630 def stream(self) -> ResponseStream:631 """The response iterable as write-only stream."""632 return ResponseStream(self)633 634 def _wrap_range_response(self, start: int, length: int) -> None:635 """Wrap existing Response in case of Range Request context."""636 if self.status_code == 206:637 self.response = _RangeWrapper(self.response, start, length) # type: ignore638 639 def _is_range_request_processable(self, environ: WSGIEnvironment) -> bool:640 """Return ``True`` if `Range` header is present and if underlying641 resource is considered unchanged when compared with `If-Range` header.642 """643 return (644 "HTTP_IF_RANGE" not in environ645 or not is_resource_modified(646 environ,647 self.headers.get("etag"),648 None,649 self.headers.get("last-modified"),650 ignore_if_range=False,651 )652 ) and "HTTP_RANGE" in environ653 654 def _process_range_request(655 self,656 environ: WSGIEnvironment,657 complete_length: int | None,658 accept_ranges: bool | str,659 ) -> bool:660 """Handle Range Request related headers (RFC7233). If `Accept-Ranges`661 header is valid, and Range Request is processable, we set the headers662 as described by the RFC, and wrap the underlying response in a663 RangeWrapper.664 665 Returns ``True`` if Range Request can be fulfilled, ``False`` otherwise.666 667 :raises: :class:`~werkzeug.exceptions.RequestedRangeNotSatisfiable`668 if `Range` header could not be parsed or satisfied.669 670 .. versionchanged:: 3.2671 Adds the ``Accept-Ranges`` header if ``accept_ranges`` is passed,672 even if this is not a satisfiable range request.673 674 .. versionchanged:: 2.0675 Returns ``False`` if the length is 0.676 """677 from ..exceptions import RequestedRangeNotSatisfiable678 679 if not accept_ranges:680 return False681 682 if accept_ranges is True:683 accept_ranges = "bytes"684 685 self.accept_ranges = accept_ranges686 687 if not (complete_length and self._is_range_request_processable(environ)):688 return False689 690 parsed_range = parse_range_header(environ.get("HTTP_RANGE"))691 692 if parsed_range is None:693 raise RequestedRangeNotSatisfiable(complete_length)694 695 range_tuple = parsed_range.range_for_length(complete_length)696 content_range_header = parsed_range.to_content_range_header(complete_length)697 698 if range_tuple is None or content_range_header is None:699 raise RequestedRangeNotSatisfiable(complete_length)700 701 content_length = range_tuple[1] - range_tuple[0]702 self.content_length = content_length703 self.content_range = content_range_header704 self.status_code = 206705 self._wrap_range_response(range_tuple[0], content_length)706 return True707 708 def make_conditional(709 self,710 request_or_environ: WSGIEnvironment | Request,711 accept_ranges: bool | str = False,712 complete_length: int | None = None,713 ) -> Response:714 """Make the response conditional to the request. This method works715 best if an etag was defined for the response already. The `add_etag`716 method can be used to do that. If called without etag just the date717 header is set.718 719 This does nothing if the request method in the request or environ is720 anything but GET or HEAD.721 722 For optimal performance when handling range requests, it's recommended723 that your response data object implements `seekable`, `seek` and `tell`724 methods as described by :py:class:`io.IOBase`. Objects returned by725 :meth:`~werkzeug.wsgi.wrap_file` automatically implement those methods.726 727 It does not remove the body of the response because that's something728 the :meth:`__call__` function does for us automatically.729 730 Returns self so that you can do ``return resp.make_conditional(req)``731 but modifies the object in-place.732 733 :param request_or_environ: a request object or WSGI environment to be734 used to make the response conditional735 against.736 :param accept_ranges: This parameter dictates the value of737 `Accept-Ranges` header. If ``False`` (default),738 the header is not set. If ``True``, it will be set739 to ``"bytes"``. If it's a string, it will use this740 value.741 :param complete_length: Will be used only in valid Range Requests.742 It will set `Content-Range` complete length743 value and compute `Content-Length` real value.744 This parameter is mandatory for successful745 Range Requests completion.746 :raises: :class:`~werkzeug.exceptions.RequestedRangeNotSatisfiable`747 if `Range` header could not be parsed or satisfied.748 749 .. versionchangedd: 3.2750 Adds the ``Accept-Ranges`` header if ``accept_ranges`` is passed,751 even if this is not a satisfiable range request.752 753 .. versionchanged:: 2.0754 Range processing is skipped if length is 0 instead of755 raising a 416 Range Not Satisfiable error.756 """757 environ = _get_environ(request_or_environ)758 if environ["REQUEST_METHOD"] in ("GET", "HEAD"):759 # if the date is not in the headers, add it now. We however760 # will not override an already existing header. Unfortunately761 # this header will be overridden by many WSGI servers including762 # wsgiref.763 if "date" not in self.headers:764 self.headers["Date"] = http_date()765 is206 = self._process_range_request(environ, complete_length, accept_ranges)766 if not is206 and not is_resource_modified(767 environ,768 self.headers.get("etag"),769 None,770 self.headers.get("last-modified"),771 ):772 if parse_etags(environ.get("HTTP_IF_MATCH")):773 self.status_code = 412774 else:775 self.status_code = 304776 if (777 self.automatically_set_content_length778 and "content-length" not in self.headers779 ):780 length = self.calculate_content_length()781 if length is not None:782 self.headers["Content-Length"] = str(length)783 return self784 785 def add_etag(self, overwrite: bool = False, weak: bool = False) -> None:786 """Add an etag for the current response if there is none yet.787 788 .. versionchanged:: 2.0789 SHA-1 is used to generate the value. MD5 may not be790 available in some environments.791 """792 if overwrite or "etag" not in self.headers:793 self.set_etag(generate_etag(self.get_data()), weak)794 795 796class ResponseStream:797 """A file descriptor like object used by :meth:`Response.stream` to798 represent the body of the stream. It directly pushes into the799 response iterable of the response object.800 """801 802 mode = "wb+"803 804 def __init__(self, response: Response):805 self.response = response806 self.closed = False807 808 def write(self, value: bytes) -> int:809 if self.closed:810 raise ValueError("I/O operation on closed file")811 self.response._ensure_sequence(mutable=True)812 self.response.response.append(value) # type: ignore813 self.response.headers.pop("Content-Length", None)814 return len(value)815 816 def writelines(self, seq: t.Iterable[bytes]) -> None:817 for item in seq:818 self.write(item)819 820 def close(self) -> None:821 self.closed = True822 823 def flush(self) -> None:824 if self.closed:825 raise ValueError("I/O operation on closed file")826 827 def isatty(self) -> bool:828 if self.closed:829 raise ValueError("I/O operation on closed file")830 return False831 832 def tell(self) -> int:833 self.response._ensure_sequence()834 return sum(map(len, self.response.response))835 836 @property837 def encoding(self) -> str:838 return "utf-8"839 