Skip to content

Commit 0a63a6e

Browse files
oskipaHugo EstradaKludexflorimondmanca
authored
Support str and datetime on expires parameter on the set_cookie method (#1908)
Co-authored-by: Hugo Estrada <hugoestrada@cal.berkeley.edu> Co-authored-by: Marcelo Trylesinski <marcelotryle@gmail.com> Co-authored-by: Florimond Manca <florimond.manca@protonmail.com>
1 parent 94a22b8 commit 0a63a6e

File tree

3 files changed

+47
-5
lines changed

3 files changed

+47
-5
lines changed

docs/responses.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ Signature: `Response.set_cookie(key, value, max_age=None, expires=None, path="/"
3636
* `key` - A string that will be the cookie's key.
3737
* `value` - A string that will be the cookie's value.
3838
* `max_age` - An integer that defines the lifetime of the cookie in seconds. A negative integer or a value of `0` will discard the cookie immediately. `Optional`
39-
* `expires` - An integer that defines the number of seconds until the cookie expires. `Optional`
39+
* `expires` - Either an integer that defines the number of seconds until the cookie expires, or a datetime. `Optional`
4040
* `path` - A string that specifies the subset of routes to which the cookie will apply. `Optional`
4141
* `domain` - A string that specifies the domain for which the cookie is valid. `Optional`
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`

starlette/responses.py

+7-3
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44
import stat
55
import sys
66
import typing
7-
from email.utils import formatdate
7+
from datetime import datetime
8+
from email.utils import format_datetime, formatdate
89
from functools import partial
910
from mimetypes import guess_type as mimetypes_guess_type
1011
from urllib.parse import quote
@@ -105,7 +106,7 @@ def set_cookie(
105106
key: str,
106107
value: str = "",
107108
max_age: typing.Optional[int] = None,
108-
expires: typing.Optional[int] = None,
109+
expires: typing.Optional[typing.Union[datetime, str, int]] = None,
109110
path: str = "/",
110111
domain: typing.Optional[str] = None,
111112
secure: bool = False,
@@ -117,7 +118,10 @@ def set_cookie(
117118
if max_age is not None:
118119
cookie[key]["max-age"] = max_age
119120
if expires is not None:
120-
cookie[key]["expires"] = expires
121+
if isinstance(expires, datetime):
122+
cookie[key]["expires"] = format_datetime(expires, usegmt=True)
123+
else:
124+
cookie[key]["expires"] = expires
121125
if path is not None:
122126
cookie[key]["path"] = path
123127
if domain is not None:

tests/test_responses.py

+39-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1+
import datetime as dt
12
import os
3+
import time
4+
from http.cookies import SimpleCookie
25

36
import anyio
47
import pytest
@@ -288,7 +291,11 @@ def test_file_response_with_inline_disposition(tmpdir, test_client_factory):
288291
assert response.headers["content-disposition"] == expected_disposition
289292

290293

291-
def test_set_cookie(test_client_factory):
294+
def test_set_cookie(test_client_factory, monkeypatch):
295+
# Mock time used as a reference for `Expires` by stdlib `SimpleCookie`.
296+
mocked_now = dt.datetime(2100, 1, 22, 12, 0, 0, tzinfo=dt.timezone.utc)
297+
monkeypatch.setattr(time, "time", lambda: mocked_now.timestamp())
298+
292299
async def app(scope, receive, send):
293300
response = Response("Hello, world!", media_type="text/plain")
294301
response.set_cookie(
@@ -307,6 +314,37 @@ async def app(scope, receive, send):
307314
client = test_client_factory(app)
308315
response = client.get("/")
309316
assert response.text == "Hello, world!"
317+
assert (
318+
response.headers["set-cookie"]
319+
== "mycookie=myvalue; Domain=localhost; expires=Fri, 22 Jan 2100 12:00:10 GMT; "
320+
"HttpOnly; Max-Age=10; Path=/; SameSite=none; Secure"
321+
)
322+
323+
324+
@pytest.mark.parametrize(
325+
"expires",
326+
[
327+
pytest.param(
328+
dt.datetime(2100, 1, 22, 12, 0, 10, tzinfo=dt.timezone.utc), id="datetime"
329+
),
330+
pytest.param("Fri, 22 Jan 2100 12:00:10 GMT", id="str"),
331+
pytest.param(10, id="int"),
332+
],
333+
)
334+
def test_expires_on_set_cookie(test_client_factory, monkeypatch, expires):
335+
# Mock time used as a reference for `Expires` by stdlib `SimpleCookie`.
336+
mocked_now = dt.datetime(2100, 1, 22, 12, 0, 0, tzinfo=dt.timezone.utc)
337+
monkeypatch.setattr(time, "time", lambda: mocked_now.timestamp())
338+
339+
async def app(scope, receive, send):
340+
response = Response("Hello, world!", media_type="text/plain")
341+
response.set_cookie("mycookie", "myvalue", expires=expires)
342+
await response(scope, receive, send)
343+
344+
client = test_client_factory(app)
345+
response = client.get("/")
346+
cookie: SimpleCookie = SimpleCookie(response.headers.get("set-cookie"))
347+
assert cookie["mycookie"]["expires"] == "Fri, 22 Jan 2100 12:00:10 GMT"
310348

311349

312350
def test_delete_cookie(test_client_factory):

0 commit comments

Comments
 (0)