1+ import calendar
12import contextlib
23import datetime
34import os # noqa
45import pathlib
56import pickle
67import re
8+ import time
79import warnings
810from collections import defaultdict
911from http .cookies import BaseCookie , Morsel , SimpleCookie
12+ from math import ceil
1013from typing import ( # noqa
1114 DefaultDict ,
1215 Dict ,
2427from yarl import URL
2528
2629from .abc import AbstractCookieJar , ClearCookiePredicate
27- from .helpers import is_ip_address , next_whole_second
30+ from .helpers import is_ip_address
2831from .typedefs import LooseCookies , PathLike , StrOrURL
2932
3033__all__ = ("CookieJar" , "DummyCookieJar" )
@@ -52,9 +55,22 @@ class CookieJar(AbstractCookieJar):
5255
5356 DATE_YEAR_RE = re .compile (r"(\d{2,4})" )
5457
55- MAX_TIME = datetime .datetime .max .replace (tzinfo = datetime .timezone .utc )
56-
57- MAX_32BIT_TIME = datetime .datetime .fromtimestamp (2 ** 31 - 1 , datetime .timezone .utc )
58+ # calendar.timegm() fails for timestamps after datetime.datetime.max
59+ # Minus one as a loss of precision occurs when timestamp() is called.
60+ MAX_TIME = (
61+ int (datetime .datetime .max .replace (tzinfo = datetime .timezone .utc ).timestamp ()) - 1
62+ )
63+ try :
64+ calendar .timegm (time .gmtime (MAX_TIME ))
65+ except OSError :
66+ # Hit the maximum representable time on Windows
67+ # https://learn.microsoft.com/en-us/cpp/c-runtime-library/reference/localtime-localtime32-localtime64
68+ MAX_TIME = calendar .timegm ((3000 , 12 , 31 , 23 , 59 , 59 , - 1 , - 1 , - 1 ))
69+ except OverflowError :
70+ # #4515: datetime.max may not be representable on 32-bit platforms
71+ MAX_TIME = 2 ** 31 - 1
72+ # Avoid minuses in the future, 3x faster
73+ SUB_MAX_TIME = MAX_TIME - 1
5874
5975 def __init__ (
6076 self ,
@@ -81,14 +97,8 @@ def __init__(
8197 for url in treat_as_secure_origin
8298 ]
8399 self ._treat_as_secure_origin = treat_as_secure_origin
84- self ._next_expiration = next_whole_second ()
85- self ._expirations : Dict [Tuple [str , str , str ], datetime .datetime ] = {}
86- # #4515: datetime.max may not be representable on 32-bit platforms
87- self ._max_time = self .MAX_TIME
88- try :
89- self ._max_time .timestamp ()
90- except OverflowError :
91- self ._max_time = self .MAX_32BIT_TIME
100+ self ._next_expiration : float = ceil (time .time ())
101+ self ._expirations : Dict [Tuple [str , str , str ], float ] = {}
92102
93103 def save (self , file_path : PathLike ) -> None :
94104 file_path = pathlib .Path (file_path )
@@ -102,14 +112,14 @@ def load(self, file_path: PathLike) -> None:
102112
103113 def clear (self , predicate : Optional [ClearCookiePredicate ] = None ) -> None :
104114 if predicate is None :
105- self ._next_expiration = next_whole_second ( )
115+ self ._next_expiration = ceil ( time . time () )
106116 self ._cookies .clear ()
107117 self ._host_only_cookies .clear ()
108118 self ._expirations .clear ()
109119 return
110120
111121 to_del = []
112- now = datetime . datetime . now ( datetime . timezone . utc )
122+ now = time . time ( )
113123 for (domain , path ), cookie in self ._cookies .items ():
114124 for name , morsel in cookie .items ():
115125 key = (domain , path , name )
@@ -125,13 +135,11 @@ def clear(self, predicate: Optional[ClearCookiePredicate] = None) -> None:
125135 del self ._expirations [(domain , path , name )]
126136 self ._cookies [(domain , path )].pop (name , None )
127137
128- next_expiration = min (self ._expirations .values (), default = self ._max_time )
129- try :
130- self ._next_expiration = next_expiration .replace (
131- microsecond = 0
132- ) + datetime .timedelta (seconds = 1 )
133- except OverflowError :
134- self ._next_expiration = self ._max_time
138+ self ._next_expiration = (
139+ min (* self ._expirations .values (), self .SUB_MAX_TIME ) + 1
140+ if self ._expirations
141+ else self .MAX_TIME
142+ )
135143
136144 def clear_domain (self , domain : str ) -> None :
137145 self .clear (lambda x : self ._is_domain_match (domain , x ["domain" ]))
@@ -147,9 +155,7 @@ def __len__(self) -> int:
147155 def _do_expiration (self ) -> None :
148156 self .clear (lambda x : False )
149157
150- def _expire_cookie (
151- self , when : datetime .datetime , domain : str , path : str , name : str
152- ) -> None :
158+ def _expire_cookie (self , when : float , domain : str , path : str , name : str ) -> None :
153159 self ._next_expiration = min (self ._next_expiration , when )
154160 self ._expirations [(domain , path , name )] = when
155161
@@ -207,12 +213,7 @@ def update_cookies(self, cookies: LooseCookies, response_url: URL = URL()) -> No
207213 if max_age :
208214 try :
209215 delta_seconds = int (max_age )
210- try :
211- max_age_expiration = datetime .datetime .now (
212- datetime .timezone .utc
213- ) + datetime .timedelta (seconds = delta_seconds )
214- except OverflowError :
215- max_age_expiration = self ._max_time
216+ max_age_expiration = min (time .time () + delta_seconds , self .MAX_TIME )
216217 self ._expire_cookie (max_age_expiration , domain , path , name )
217218 except ValueError :
218219 cookie ["max-age" ] = ""
@@ -328,7 +329,7 @@ def _is_path_match(req_path: str, cookie_path: str) -> bool:
328329 return non_matching .startswith ("/" )
329330
330331 @classmethod
331- def _parse_date (cls , date_str : str ) -> Optional [datetime . datetime ]:
332+ def _parse_date (cls , date_str : str ) -> Optional [int ]:
332333 """Implements date string parsing adhering to RFC 6265."""
333334 if not date_str :
334335 return None
@@ -388,9 +389,7 @@ def _parse_date(cls, date_str: str) -> Optional[datetime.datetime]:
388389 if year < 1601 or hour > 23 or minute > 59 or second > 59 :
389390 return None
390391
391- return datetime .datetime (
392- year , month , day , hour , minute , second , tzinfo = datetime .timezone .utc
393- )
392+ return calendar .timegm ((year , month , day , hour , minute , second , - 1 , - 1 , - 1 ))
394393
395394
396395class DummyCookieJar (AbstractCookieJar ):
0 commit comments