Skip to content

Commit 66e281f

Browse files
asvetlovderlih
andauthored
[3.8] Cookie jar delete specific cookie (#5280)
Co-authored-by: Andrew Svetlov <[email protected]>. (cherry picked from commit e65f1a9) Co-authored-by: Dmitry Erlikh <[email protected]>
1 parent d88aa16 commit 66e281f

File tree

6 files changed

+129
-34
lines changed

6 files changed

+129
-34
lines changed

CHANGES/4942.feature

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Add predicate to ``AbstractCookieJar.clear``.
2+
Add ``AbstractCookieJar.clear_domain`` to clean all domain and subdomains cookies only.

aiohttp/abc.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -135,15 +135,22 @@ async def close(self) -> None:
135135
IterableBase = Iterable
136136

137137

138+
ClearCookiePredicate = Callable[["Morsel[str]"], bool]
139+
140+
138141
class AbstractCookieJar(Sized, IterableBase):
139142
"""Abstract Cookie Jar."""
140143

141144
def __init__(self, *, loop: Optional[asyncio.AbstractEventLoop] = None) -> None:
142145
self._loop = get_running_loop(loop)
143146

144147
@abstractmethod
145-
def clear(self) -> None:
146-
"""Clear all cookies."""
148+
def clear(self, predicate: Optional[ClearCookiePredicate] = None) -> None:
149+
"""Clear all cookies if no predicate is passed."""
150+
151+
@abstractmethod
152+
def clear_domain(self, domain: str) -> None:
153+
"""Clear all cookies for domain and all subdomains."""
147154

148155
@abstractmethod
149156
def update_cookies(self, cookies: LooseCookies, response_url: URL = URL()) -> None:

aiohttp/cookiejar.py

Lines changed: 41 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121

2222
from yarl import URL
2323

24-
from .abc import AbstractCookieJar
24+
from .abc import AbstractCookieJar, ClearCookiePredicate
2525
from .helpers import is_ip_address, next_whole_second
2626
from .typedefs import LooseCookies, PathLike
2727

@@ -87,11 +87,41 @@ def load(self, file_path: PathLike) -> None:
8787
with file_path.open(mode="rb") as f:
8888
self._cookies = pickle.load(f)
8989

90-
def clear(self) -> None:
91-
self._cookies.clear()
92-
self._host_only_cookies.clear()
93-
self._next_expiration = next_whole_second()
94-
self._expirations.clear()
90+
def clear(self, predicate: Optional[ClearCookiePredicate] = None) -> None:
91+
if predicate is None:
92+
self._next_expiration = next_whole_second()
93+
self._cookies.clear()
94+
self._host_only_cookies.clear()
95+
self._expirations.clear()
96+
return
97+
98+
to_del = []
99+
now = datetime.datetime.now(datetime.timezone.utc)
100+
for domain, cookie in self._cookies.items():
101+
for name, morsel in cookie.items():
102+
key = (domain, name)
103+
if (
104+
key in self._expirations and self._expirations[key] <= now
105+
) or predicate(morsel):
106+
to_del.append(key)
107+
108+
for domain, name in to_del:
109+
key = (domain, name)
110+
self._host_only_cookies.discard(key)
111+
if key in self._expirations:
112+
del self._expirations[(domain, name)]
113+
self._cookies[domain].pop(name, None)
114+
115+
next_expiration = min(self._expirations.values(), default=self._max_time)
116+
try:
117+
self._next_expiration = next_expiration.replace(
118+
microsecond=0
119+
) + datetime.timedelta(seconds=1)
120+
except OverflowError:
121+
self._next_expiration = self._max_time
122+
123+
def clear_domain(self, domain: str) -> None:
124+
self.clear(lambda x: self._is_domain_match(domain, x["domain"]))
95125

96126
def __iter__(self) -> "Iterator[Morsel[str]]":
97127
self._do_expiration()
@@ -102,31 +132,7 @@ def __len__(self) -> int:
102132
return sum(1 for i in self)
103133

104134
def _do_expiration(self) -> None:
105-
now = datetime.datetime.now(datetime.timezone.utc)
106-
if self._next_expiration > now:
107-
return
108-
if not self._expirations:
109-
return
110-
next_expiration = self._max_time
111-
to_del = []
112-
cookies = self._cookies
113-
expirations = self._expirations
114-
for (domain, name), when in expirations.items():
115-
if when <= now:
116-
cookies[domain].pop(name, None)
117-
to_del.append((domain, name))
118-
self._host_only_cookies.discard((domain, name))
119-
else:
120-
next_expiration = min(next_expiration, when)
121-
for key in to_del:
122-
del expirations[key]
123-
124-
try:
125-
self._next_expiration = next_expiration.replace(
126-
microsecond=0
127-
) + datetime.timedelta(seconds=1)
128-
except OverflowError:
129-
self._next_expiration = self._max_time
135+
self.clear(lambda x: False)
130136

131137
def _expire_cookie(self, when: datetime.datetime, domain: str, name: str) -> None:
132138
self._next_expiration = min(self._next_expiration, when)
@@ -372,7 +378,10 @@ def __iter__(self) -> "Iterator[Morsel[str]]":
372378
def __len__(self) -> int:
373379
return 0
374380

375-
def clear(self) -> None:
381+
def clear(self, predicate: Optional[ClearCookiePredicate] = None) -> None:
382+
pass
383+
384+
def clear_domain(self, domain: str) -> None:
376385
pass
377386

378387
def update_cookies(self, cookies: LooseCookies, response_url: URL = URL()) -> None:

docs/abc.rst

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,22 @@ Abstract Cookie Jar
145145
:return: :class:`http.cookies.SimpleCookie` with filtered
146146
cookies for given URL.
147147

148+
.. method:: clear(predicate=None)
149+
150+
Removes all cookies from the jar if the predicate is ``None``. Otherwise remove only those :class:`~http.cookies.Morsel` that ``predicate(morsel)`` returns ``True``.
151+
152+
:param predicate: callable that gets :class:`~http.cookies.Morsel` as a parameter and returns ``True`` if this :class:`~http.cookies.Morsel` must be deleted from the jar.
153+
154+
.. versionadded:: 3.8
155+
156+
.. method:: clear_domain(domain)
157+
158+
Remove all cookies from the jar that belongs to the specified domain or its subdomains.
159+
160+
:param str domain: domain for which cookies must be deleted from the jar.
161+
162+
.. versionadded:: 3.8
163+
148164
Abstract Abstract Access Logger
149165
-------------------------------
150166

docs/client_reference.rst

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1839,6 +1839,22 @@ CookieJar
18391839
:param file_path: Path to file from where cookies will be
18401840
imported, :class:`str` or :class:`pathlib.Path` instance.
18411841

1842+
.. method:: clear(predicate=None)
1843+
1844+
Removes all cookies from the jar if the predicate is ``None``. Otherwise remove only those :class:`~http.cookies.Morsel` that ``predicate(morsel)`` returns ``True``.
1845+
1846+
:param predicate: callable that gets :class:`~http.cookies.Morsel` as a parameter and returns ``True`` if this :class:`~http.cookies.Morsel` must be deleted from the jar.
1847+
1848+
.. versionadded:: 4.0
1849+
1850+
.. method:: clear_domain(domain)
1851+
1852+
Remove all cookies from the jar that belongs to the specified domain or its subdomains.
1853+
1854+
:param str domain: domain for which cookies must be deleted from the jar.
1855+
1856+
.. versionadded:: 4.0
1857+
18421858

18431859
.. class:: DummyCookieJar(*, loop=None)
18441860

tests/test_cookiejar.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -694,3 +694,48 @@ async def test_loose_cookies_types() -> None:
694694

695695
for loose_cookies_type in accepted_types:
696696
jar.update_cookies(cookies=loose_cookies_type)
697+
698+
699+
async def test_cookie_jar_clear_all():
700+
sut = CookieJar()
701+
cookie = SimpleCookie()
702+
cookie["foo"] = "bar"
703+
sut.update_cookies(cookie)
704+
705+
sut.clear()
706+
assert len(sut) == 0
707+
708+
709+
async def test_cookie_jar_clear_expired():
710+
sut = CookieJar()
711+
712+
cookie = SimpleCookie()
713+
714+
cookie["foo"] = "bar"
715+
cookie["foo"]["expires"] = "Tue, 1 Jan 1990 12:00:00 GMT"
716+
717+
with freeze_time("1980-01-01"):
718+
sut.update_cookies(cookie)
719+
720+
sut.clear(lambda x: False)
721+
with freeze_time("1980-01-01"):
722+
assert len(sut) == 0
723+
724+
725+
async def test_cookie_jar_clear_domain():
726+
sut = CookieJar()
727+
cookie = SimpleCookie()
728+
cookie["foo"] = "bar"
729+
cookie["domain_cookie"] = "value"
730+
cookie["domain_cookie"]["domain"] = "example.com"
731+
cookie["subdomain_cookie"] = "value"
732+
cookie["subdomain_cookie"]["domain"] = "test.example.com"
733+
sut.update_cookies(cookie)
734+
735+
sut.clear_domain("example.com")
736+
iterator = iter(sut)
737+
morsel = next(iterator)
738+
assert morsel.key == "foo"
739+
assert morsel.value == "bar"
740+
with pytest.raises(StopIteration):
741+
next(iterator)

0 commit comments

Comments
 (0)