Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/pip/_vendor/cachecontrol/serialize.py: 23%
77 statements
« prev ^ index » next coverage.py v7.4.3, created at 2024-02-26 06:33 +0000
« prev ^ index » next coverage.py v7.4.3, created at 2024-02-26 06:33 +0000
1# SPDX-FileCopyrightText: 2015 Eric Larson
2#
3# SPDX-License-Identifier: Apache-2.0
4from __future__ import annotations
6import io
7from typing import IO, TYPE_CHECKING, Any, Mapping, cast
9from pip._vendor import msgpack
10from pip._vendor.requests.structures import CaseInsensitiveDict
11from pip._vendor.urllib3 import HTTPResponse
13if TYPE_CHECKING:
14 from pip._vendor.requests import PreparedRequest
17class Serializer:
18 serde_version = "4"
20 def dumps(
21 self,
22 request: PreparedRequest,
23 response: HTTPResponse,
24 body: bytes | None = None,
25 ) -> bytes:
26 response_headers: CaseInsensitiveDict[str] = CaseInsensitiveDict(
27 response.headers
28 )
30 if body is None:
31 # When a body isn't passed in, we'll read the response. We
32 # also update the response with a new file handler to be
33 # sure it acts as though it was never read.
34 body = response.read(decode_content=False)
35 response._fp = io.BytesIO(body) # type: ignore[attr-defined]
36 response.length_remaining = len(body)
38 data = {
39 "response": {
40 "body": body, # Empty bytestring if body is stored separately
41 "headers": {str(k): str(v) for k, v in response.headers.items()}, # type: ignore[no-untyped-call]
42 "status": response.status,
43 "version": response.version,
44 "reason": str(response.reason),
45 "decode_content": response.decode_content,
46 }
47 }
49 # Construct our vary headers
50 data["vary"] = {}
51 if "vary" in response_headers:
52 varied_headers = response_headers["vary"].split(",")
53 for header in varied_headers:
54 header = str(header).strip()
55 header_value = request.headers.get(header, None)
56 if header_value is not None:
57 header_value = str(header_value)
58 data["vary"][header] = header_value
60 return b",".join([f"cc={self.serde_version}".encode(), self.serialize(data)])
62 def serialize(self, data: dict[str, Any]) -> bytes:
63 return cast(bytes, msgpack.dumps(data, use_bin_type=True))
65 def loads(
66 self,
67 request: PreparedRequest,
68 data: bytes,
69 body_file: IO[bytes] | None = None,
70 ) -> HTTPResponse | None:
71 # Short circuit if we've been given an empty set of data
72 if not data:
73 return None
75 # Determine what version of the serializer the data was serialized
76 # with
77 try:
78 ver, data = data.split(b",", 1)
79 except ValueError:
80 ver = b"cc=0"
82 # Make sure that our "ver" is actually a version and isn't a false
83 # positive from a , being in the data stream.
84 if ver[:3] != b"cc=":
85 data = ver + data
86 ver = b"cc=0"
88 # Get the version number out of the cc=N
89 verstr = ver.split(b"=", 1)[-1].decode("ascii")
91 # Dispatch to the actual load method for the given version
92 try:
93 return getattr(self, f"_loads_v{verstr}")(request, data, body_file) # type: ignore[no-any-return]
95 except AttributeError:
96 # This is a version we don't have a loads function for, so we'll
97 # just treat it as a miss and return None
98 return None
100 def prepare_response(
101 self,
102 request: PreparedRequest,
103 cached: Mapping[str, Any],
104 body_file: IO[bytes] | None = None,
105 ) -> HTTPResponse | None:
106 """Verify our vary headers match and construct a real urllib3
107 HTTPResponse object.
108 """
109 # Special case the '*' Vary value as it means we cannot actually
110 # determine if the cached response is suitable for this request.
111 # This case is also handled in the controller code when creating
112 # a cache entry, but is left here for backwards compatibility.
113 if "*" in cached.get("vary", {}):
114 return None
116 # Ensure that the Vary headers for the cached response match our
117 # request
118 for header, value in cached.get("vary", {}).items():
119 if request.headers.get(header, None) != value:
120 return None
122 body_raw = cached["response"].pop("body")
124 headers: CaseInsensitiveDict[str] = CaseInsensitiveDict(
125 data=cached["response"]["headers"]
126 )
127 if headers.get("transfer-encoding", "") == "chunked":
128 headers.pop("transfer-encoding")
130 cached["response"]["headers"] = headers
132 try:
133 body: IO[bytes]
134 if body_file is None:
135 body = io.BytesIO(body_raw)
136 else:
137 body = body_file
138 except TypeError:
139 # This can happen if cachecontrol serialized to v1 format (pickle)
140 # using Python 2. A Python 2 str(byte string) will be unpickled as
141 # a Python 3 str (unicode string), which will cause the above to
142 # fail with:
143 #
144 # TypeError: 'str' does not support the buffer interface
145 body = io.BytesIO(body_raw.encode("utf8"))
147 # Discard any `strict` parameter serialized by older version of cachecontrol.
148 cached["response"].pop("strict", None)
150 return HTTPResponse(body=body, preload_content=False, **cached["response"])
152 def _loads_v0(
153 self,
154 request: PreparedRequest,
155 data: bytes,
156 body_file: IO[bytes] | None = None,
157 ) -> None:
158 # The original legacy cache data. This doesn't contain enough
159 # information to construct everything we need, so we'll treat this as
160 # a miss.
161 return None
163 def _loads_v1(
164 self,
165 request: PreparedRequest,
166 data: bytes,
167 body_file: IO[bytes] | None = None,
168 ) -> HTTPResponse | None:
169 # The "v1" pickled cache format. This is no longer supported
170 # for security reasons, so we treat it as a miss.
171 return None
173 def _loads_v2(
174 self,
175 request: PreparedRequest,
176 data: bytes,
177 body_file: IO[bytes] | None = None,
178 ) -> HTTPResponse | None:
179 # The "v2" compressed base64 cache format.
180 # This has been removed due to age and poor size/performance
181 # characteristics, so we treat it as a miss.
182 return None
184 def _loads_v3(
185 self,
186 request: PreparedRequest,
187 data: bytes,
188 body_file: IO[bytes] | None = None,
189 ) -> None:
190 # Due to Python 2 encoding issues, it's impossible to know for sure
191 # exactly how to load v3 entries, thus we'll treat these as a miss so
192 # that they get rewritten out as v4 entries.
193 return None
195 def _loads_v4(
196 self,
197 request: PreparedRequest,
198 data: bytes,
199 body_file: IO[bytes] | None = None,
200 ) -> HTTPResponse | None:
201 try:
202 cached = msgpack.loads(data, raw=False)
203 except ValueError:
204 return None
206 return self.prepare_response(request, cached, body_file)