3939_MIN_SCHEDULED_COOKIE_EXPIRATION = 100
4040_SIMPLE_COOKIE = SimpleCookie ()
4141
42+ # Not persisted; the absolute deadline is saved instead.
43+ _RELATIVE_EXPIRY_ATTRS = frozenset (("max-age" , "expires" ))
44+
4245
4346class _RestrictedCookieUnpickler (pickle ._Unpickler ):
4447 """A restricted unpickler that only allows cookie-related types.
@@ -174,21 +177,28 @@ def save(self, file_path: PathLike) -> None:
174177 :class:`str` or :class:`pathlib.Path` instance.
175178 """
176179 file_path = pathlib .Path (file_path )
177- data : dict [str , dict [str , dict [str , str | bool ]]] = {}
180+ data : dict [str , dict [str , dict [str , str | bool | float ]]] = {}
178181 for (domain , path ), cookie in self ._cookies .items ():
179182 key = f"{ domain } |{ path } "
180183 data [key ] = {}
181184 for name , morsel in cookie .items ():
182- morsel_data : dict [str , str | bool ] = {
185+ morsel_data : dict [str , str | bool | float ] = {
183186 "key" : morsel .key ,
184187 "value" : morsel .value ,
185188 "coded_value" : morsel .coded_value ,
186189 }
187- # Save all morsel attributes that have values
190+ # Skip relative expiry; the absolute deadline is saved below.
188191 for attr in morsel ._reserved : # type: ignore[attr-defined]
192+ if attr in _RELATIVE_EXPIRY_ATTRS :
193+ continue
189194 attr_val = morsel [attr ]
190195 if attr_val :
191196 morsel_data [attr ] = attr_val
197+ # Persist or it reloads as a domain cookie and leaks to subdomains.
198+ if (domain , name ) in self ._host_only_cookies :
199+ morsel_data ["host_only" ] = True
200+ if (exp := self ._expirations .get ((domain , path , name ))) is not None :
201+ morsel_data ["expires_timestamp" ] = exp
192202 data [key ][name ] = morsel_data
193203
194204 # Cookie persistence may include authentication/session tokens.
@@ -209,6 +219,9 @@ def load(self, file_path: PathLike) -> None:
209219 pickle format (using a restricted unpickler) for backward
210220 compatibility with existing cookie files.
211221
222+ Replaces the current jar contents; loaded cookies pass through the
223+ same acceptance rules as :meth:`update_cookies`.
224+
212225 :param file_path: Path to file from where cookies will be
213226 imported, :class:`str` or :class:`pathlib.Path` instance.
214227 """
@@ -217,32 +230,28 @@ def load(self, file_path: PathLike) -> None:
217230 try :
218231 with file_path .open (mode = "r" , encoding = "utf-8" ) as f :
219232 data = json .load (f )
220- self ._cookies = self . _load_json_data (data )
233+ self ._load_json_data (data )
221234 except (json .JSONDecodeError , UnicodeDecodeError , ValueError ):
222235 # Fall back to legacy pickle format with restricted unpickler
223236 with file_path .open (mode = "rb" ) as f :
224237 self ._cookies = _RestrictedCookieUnpickler (f ).load ()
225238
226239 def _load_json_data (
227- self , data : dict [str , dict [str , dict [str , str | bool ]]]
228- ) -> defaultdict [ tuple [ str , str ], SimpleCookie ] :
229- """Load cookies from parsed JSON data ."""
230- cookies : defaultdict [ tuple [ str , str ], SimpleCookie ] = defaultdict ( SimpleCookie )
240+ self , data : dict [str , dict [str , dict [str , str | bool | float ]]]
241+ ) -> None :
242+ """Replace contents, routing cookies through update_cookies() ."""
243+ self . clear ( )
231244 for compound_key , cookie_data in data .items ():
232245 domain , path = compound_key .split ("|" , 1 )
233- key = (domain , path )
234246 for name , morsel_data in cookie_data .items ():
235247 morsel : Morsel [str ] = Morsel ()
236- morsel_key = morsel_data ["key" ]
237- morsel_value = morsel_data ["value" ]
238- morsel_coded_value = morsel_data ["coded_value" ]
239248 # Use __setstate__ to bypass validation, same pattern
240249 # used in _build_morsel and _cookie_helpers.
241250 morsel .__setstate__ ( # type: ignore[attr-defined]
242251 {
243- "key" : morsel_key ,
244- "value" : morsel_value ,
245- "coded_value" : morsel_coded_value ,
252+ "key" : morsel_data [ "key" ] ,
253+ "value" : morsel_data [ "value" ] ,
254+ "coded_value" : morsel_data [ "coded_value" ] ,
246255 }
247256 )
248257 # Restore morsel attributes
@@ -253,8 +262,17 @@ def _load_json_data(
253262 "coded_value" ,
254263 ):
255264 morsel [attr ] = morsel_data [attr ]
256- cookies [key ][name ] = morsel
257- return cookies
265+ # Drop the domain so update_cookies() re-marks it host-only.
266+ if morsel_data .get ("host_only" ):
267+ morsel ["domain" ] = ""
268+ response_url = (
269+ URL .build (scheme = "https" , host = domain ) if domain else URL ()
270+ )
271+ self .update_cookies ({name : morsel }, response_url )
272+ # Restore the absolute deadline; update_cookies() schedules none.
273+ if (exp := morsel_data .get ("expires_timestamp" )) is not None :
274+ self ._expire_cookie (float (exp ), domain , path , name )
275+ self ._do_expiration ()
258276
259277 def clear (self , predicate : ClearCookiePredicate | None = None ) -> None :
260278 if predicate is None :
0 commit comments