Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGES/3951.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Added :py:attr:`~aiohttp.CookieJar.cookies` and :py:attr:`~aiohttp.CookieJar.host_only_cookies` read-only properties to :py:class:`~aiohttp.CookieJar` exposing the stored cookies with their full attributes -- by :user:`Br1an67`.
1 change: 1 addition & 0 deletions CONTRIBUTORS.txt
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ Bob Haddleton
Boris Feld
Borys Vorona
Boyi Chen
Br1an67
Brett Cannon
Brett Higgins
Brian Bouterse
Expand Down
13 changes: 12 additions & 1 deletion aiohttp/abc.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
import socket
from abc import ABC, abstractmethod
from collections.abc import Awaitable, Callable, Generator, Iterable, Sequence, Sized
from http.cookies import BaseCookie, Morsel
from http.cookies import BaseCookie, Morsel, SimpleCookie
from types import MappingProxyType
from typing import TYPE_CHECKING, Any, TypedDict

from multidict import CIMultiDict
Expand Down Expand Up @@ -158,6 +159,16 @@ class AbstractCookieJar(Sized, Iterable[Morsel[str]]):
def quote_cookie(self) -> bool:
"""Return True if cookies should be quoted."""

@property
@abstractmethod
def cookies(self) -> MappingProxyType[tuple[str, str], SimpleCookie]:
"""Return the cookies stored in this jar."""

@property
@abstractmethod
def host_only_cookies(self) -> frozenset[tuple[str, str]]:
"""Return the host-only cookies stored in this jar."""

@abstractmethod
def clear(self, predicate: ClearCookiePredicate | None = None) -> None:
"""Clear all cookies if no predicate is passed."""
Expand Down
21 changes: 21 additions & 0 deletions aiohttp/cookiejar.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from collections import defaultdict
from collections.abc import Iterable, Iterator, Mapping
from http.cookies import BaseCookie, Morsel, SimpleCookie
from types import MappingProxyType
from typing import Union

from yarl import URL
Expand Down Expand Up @@ -147,6 +148,16 @@ def __init__(
def quote_cookie(self) -> bool:
return self._quote_cookie

@property
def cookies(self) -> MappingProxyType[tuple[str, str], SimpleCookie]:
"""Return the cookies stored in this jar."""
return MappingProxyType(self._cookies)

@property
def host_only_cookies(self) -> frozenset[tuple[str, str]]:
"""Return the host-only cookies stored in this jar."""
return frozenset(self._host_only_cookies)

def save(self, file_path: PathLike) -> None:
"""Save cookies to a file using JSON format.

Expand Down Expand Up @@ -598,6 +609,16 @@ def __len__(self) -> int:
def quote_cookie(self) -> bool:
return True

@property
def cookies(self) -> MappingProxyType[tuple[str, str], SimpleCookie]:
"""Return an empty mapping."""
return MappingProxyType({})

@property
def host_only_cookies(self) -> frozenset[tuple[str, str]]:
"""Return an empty frozenset."""
return frozenset()

def clear(self, predicate: ClearCookiePredicate | None = None) -> None:
pass

Expand Down
15 changes: 15 additions & 0 deletions docs/client_reference.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2537,6 +2537,21 @@ Utilities

.. versionadded:: 4.0

.. attribute:: cookies

A read-only view of the jar's cookies as a
:class:`~types.MappingProxyType` mapping ``(domain, path)`` tuples
to :class:`~http.cookies.SimpleCookie` instances.

.. versionadded:: 4.0

.. attribute:: host_only_cookies

A :class:`frozenset` of ``(domain, name)`` tuples indicating which
cookies are host-only (not sent to subdomains).

.. versionadded:: 4.0


.. class:: DummyCookieJar(*, loop=None)
:canonical: aiohttp.cookiejar.DummyCookieJar
Expand Down
12 changes: 11 additions & 1 deletion tests/test_client_session.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from collections import deque
from collections.abc import Awaitable, Callable, Iterator
from http.cookies import BaseCookie, SimpleCookie
from types import SimpleNamespace
from types import MappingProxyType, SimpleNamespace
from typing import Any, NoReturn, TypedDict, cast
from unittest import mock
from uuid import uuid4
Expand Down Expand Up @@ -779,6 +779,14 @@ def __init__(self) -> None:
def quote_cookie(self) -> bool:
return True

@property
def cookies(self) -> MappingProxyType[tuple[str, str], SimpleCookie]:
return MappingProxyType({})

@property
def host_only_cookies(self) -> frozenset[tuple[str, str]]:
return frozenset()

def clear(self, predicate: abc.ClearCookiePredicate | None = None) -> None:
self._clear_mock(predicate)

Expand All @@ -800,6 +808,8 @@ def __iter__(self) -> Iterator[Any]:
jar = MockCookieJar()

assert jar.quote_cookie is True
assert jar.cookies == MappingProxyType({})
assert jar.host_only_cookies == frozenset()
assert len(jar) == 0
assert list(jar) == []
jar.clear()
Expand Down
49 changes: 49 additions & 0 deletions tests/test_cookiejar.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from http.cookies import BaseCookie, Morsel, SimpleCookie
from operator import not_
from pathlib import Path
from types import MappingProxyType
from unittest import mock

import pytest
Expand Down Expand Up @@ -780,6 +781,54 @@ async def test_dummy_cookie_jar() -> None:
dummy_jar.clear()


async def test_dummy_cookie_jar_cookies_property() -> None:
dummy_jar = DummyCookieJar()
assert dict(dummy_jar.cookies) == {}
assert dummy_jar.host_only_cookies == frozenset()


async def test_cookie_jar_cookies_property() -> None:
jar = CookieJar()
cookie = SimpleCookie(
"shared-cookie=first; domain-cookie=second; Domain=example.com; Path=/; "
)
jar.update_cookies(cookie, URL("http://example.com/"))

cookies = jar.cookies
# Should be a read-only view
assert isinstance(cookies, MappingProxyType)
# Should contain the stored cookies with their full attributes
found_names = {name for simple_cookie in cookies.values() for name in simple_cookie}
assert "shared-cookie" in found_names
assert "domain-cookie" in found_names
# Verify that domain attribute is preserved
for key, simple_cookie in cookies.items():
for name, morsel in simple_cookie.items():
if name == "domain-cookie":
assert morsel["domain"] == "example.com"
assert morsel["path"] == "/"


async def test_cookie_jar_host_only_cookies_property() -> None:
jar = CookieJar()
# Cookies without an explicit Domain attribute are host-only
cookie = SimpleCookie("hostonly=value;")
jar.update_cookies(cookie, URL("http://example.com/"))

host_only = jar.host_only_cookies
assert isinstance(host_only, frozenset)
assert ("example.com", "hostonly") in host_only


async def test_cookie_jar_cookies_property_immutable() -> None:
jar = CookieJar()
cookie = SimpleCookie("foo=bar;")
jar.update_cookies(cookie, URL("http://example.com/"))
cookies = jar.cookies
with pytest.raises(TypeError):
cookies[("new", "key")] = SimpleCookie() # type: ignore[index]


async def test_loose_cookies_types() -> None:
jar = CookieJar()

Expand Down
Loading