Skip to content

Commit 4acf1d1

Browse files
FlecartKludex
andauthored
feat: add partitioned attribute to cookie (#2501)
Co-authored-by: Marcelo Trylesinski <marcelotryle@gmail.com>
1 parent 278e33e commit 4acf1d1

File tree

3 files changed

+30
-2
lines changed

3 files changed

+30
-2
lines changed

docs/responses.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ async def app(scope, receive, send):
3131

3232
Starlette provides a `set_cookie` method to allow you to set cookies on the response object.
3333

34-
Signature: `Response.set_cookie(key, value, max_age=None, expires=None, path="/", domain=None, secure=False, httponly=False, samesite="lax")`
34+
Signature: `Response.set_cookie(key, value, max_age=None, expires=None, path="/", domain=None, secure=False, httponly=False, samesite="lax", partitioned=False)`
3535

3636
* `key` - A string that will be the cookie's key.
3737
* `value` - A string that will be the cookie's value.
@@ -42,6 +42,7 @@ Signature: `Response.set_cookie(key, value, max_age=None, expires=None, path="/"
4242
* `secure` - A bool indicating that the cookie will only be sent to the server if request is made using SSL and the HTTPS protocol. `Optional`
4343
* `httponly` - A bool indicating that the cookie cannot be accessed via JavaScript through `Document.cookie` property, the `XMLHttpRequest` or `Request` APIs. `Optional`
4444
* `samesite` - A string that specifies the samesite strategy for the cookie. Valid values are `'lax'`, `'strict'` and `'none'`. Defaults to `'lax'`. `Optional`
45+
* `partitioned` - A bool that indicates to user agents that these cross-site cookies should only be available in the same top-level context that the cookie was first set in. Only available for Python 3.14+, otherwise an error will be raised. `Optional`
4546

4647
#### Delete Cookie
4748

starlette/responses.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import os
77
import re
88
import stat
9+
import sys
910
import typing
1011
import warnings
1112
from datetime import datetime
@@ -97,6 +98,7 @@ def set_cookie(
9798
secure: bool = False,
9899
httponly: bool = False,
99100
samesite: typing.Literal["lax", "strict", "none"] | None = "lax",
101+
partitioned: bool = False,
100102
) -> None:
101103
cookie: http.cookies.BaseCookie[str] = http.cookies.SimpleCookie()
102104
cookie[key] = value
@@ -122,6 +124,11 @@ def set_cookie(
122124
"none",
123125
], "samesite must be either 'strict', 'lax' or 'none'"
124126
cookie[key]["samesite"] = samesite
127+
if partitioned:
128+
if sys.version_info < (3, 14):
129+
raise ValueError("Partitioned cookies are only supported in Python 3.14 and above.") # pragma: no cover
130+
cookie[key]["partitioned"] = True # pragma: no cover
131+
125132
cookie_val = cookie.output(header="").strip()
126133
self.raw_headers.append((b"set-cookie", cookie_val.encode("latin-1")))
127134

tests/test_responses.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from __future__ import annotations
22

33
import datetime as dt
4+
import sys
45
import time
56
from collections.abc import AsyncGenerator, AsyncIterator, Iterator
67
from http.cookies import SimpleCookie
@@ -370,18 +371,37 @@ async def app(scope: Scope, receive: Receive, send: Send) -> None:
370371
secure=True,
371372
httponly=True,
372373
samesite="none",
374+
partitioned=True if sys.version_info >= (3, 14) else False,
373375
)
374376
await response(scope, receive, send)
375377

378+
partitioned_text = "Partitioned; " if sys.version_info >= (3, 14) else ""
379+
376380
client = test_client_factory(app)
377381
response = client.get("/")
378382
assert response.text == "Hello, world!"
379383
assert (
380384
response.headers["set-cookie"] == "mycookie=myvalue; Domain=localhost; expires=Thu, 22 Jan 2037 12:00:10 GMT; "
381-
"HttpOnly; Max-Age=10; Path=/; SameSite=none; Secure"
385+
f"HttpOnly; Max-Age=10; {partitioned_text}Path=/; SameSite=none; Secure"
382386
)
383387

384388

389+
@pytest.mark.skipif(sys.version_info >= (3, 14), reason="Only relevant for <3.14")
390+
def test_set_cookie_raises_for_invalid_python_version(
391+
test_client_factory: TestClientFactory,
392+
) -> None: # pragma: no cover
393+
async def app(scope: Scope, receive: Receive, send: Send) -> None:
394+
response = Response("Hello, world!", media_type="text/plain")
395+
with pytest.raises(ValueError):
396+
response.set_cookie("mycookie", "myvalue", partitioned=True)
397+
await response(scope, receive, send)
398+
399+
client = test_client_factory(app)
400+
response = client.get("/")
401+
assert response.text == "Hello, world!"
402+
assert response.headers.get("set-cookie") is None
403+
404+
385405
def test_set_cookie_path_none(test_client_factory: TestClientFactory) -> None:
386406
async def app(scope: Scope, receive: Receive, send: Send) -> None:
387407
response = Response("Hello, world!", media_type="text/plain")

0 commit comments

Comments
 (0)