Skip to content

Support str and datetime on expires parameter on the set_cookie method#1908

Merged
Kludex merged 23 commits into
Kludex:masterfrom
oskipa:issue-1878-fix-cookie-typing
Feb 6, 2023
Merged

Support str and datetime on expires parameter on the set_cookie method#1908
Kludex merged 23 commits into
Kludex:masterfrom
oskipa:issue-1878-fix-cookie-typing

Conversation

@oskipa

@oskipa oskipa commented Oct 8, 2022

Copy link
Copy Markdown
Contributor

What?

The PR implements the type changes suggested in the discussion. We are adding typing.Optional[typing.Union[datetime, str] to set cookie.

Why?

Expiration was previously set as an integer. Reviewing https://www.rfc-editor.org/rfc/rfc6265 and https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie doesn't show that an integer is the right type for a cookie date.

How?

Following the instructions given in the discussion.

Notes

This is my first PR to this project, as part of Hacktober fest :)

Edit by @Kludex :

Missing

  • Update documentation.
  • Add test with naive timezone.
Comment thread starlette/responses.py Outdated
Comment thread starlette/responses.py Outdated
Comment thread starlette/responses.py Outdated
Comment thread starlette/responses.py Outdated
@Kludex Kludex mentioned this pull request Oct 12, 2022

@Kludex Kludex left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One test is enough. Make sure to pass a datetime, and assert the cookie has the format specified.

Comment thread starlette/responses.py Outdated
Comment thread starlette/responses.py Outdated
Comment thread tests/test_responses.py Outdated
@oskipa

oskipa commented Oct 20, 2022

Copy link
Copy Markdown
Contributor Author

The build is failing on some database test. The tests pass locally. I don't think it is related to the changes I made, but if it is, please let me know, and I can try to figure out what is going on to fix it.

I saw another PR that has the same database error.

@oskipa

oskipa commented Oct 21, 2022

Copy link
Copy Markdown
Contributor Author

The tests are passing now :)

@Kludex

Kludex commented Oct 22, 2022

Copy link
Copy Markdown
Owner

Cool. Thank you.

I just have one doubt here... Why does being an int works on Chrome? Does it work on other browsers as well?

@oskipa

oskipa commented Oct 25, 2022

Copy link
Copy Markdown
Contributor Author

Cool. Thank you.

I just have one doubt here... Why does being an int works on Chrome? Does it work on other browsers as well?

Let me investigate that this morning

@oskipa

oskipa commented Oct 25, 2022

Copy link
Copy Markdown
Contributor Author

Yes, int works in Firefox too. This behavior doesn't match the behavior described in https://www.rfc-editor.org/rfc/rfc6265#section-5.1.1 It does match the max-age behavior, which does take an integer that represents the seconds which the cookie is allowed to live.

My guess is that at some point browser programmers thought that max-age and expires were semantically the same, and allowed both integers and dates to set one or the other. In Firefox, dev tools shows expires as Expires/Max-Age. Then, although not part of the RFC, it seems to be a de-facto standard, either because the other browsers implement it that way, or because it was implemented this way years ago and no one touches the cookie code since it works.

@oskipa

oskipa commented Nov 7, 2022

Copy link
Copy Markdown
Contributor Author

Is there anything else to do?

@Kludex

Kludex commented Nov 10, 2022

Copy link
Copy Markdown
Owner

Actually, I've found out why we accept int. It's because of how SimpleCookie is implemented in CPython:

https://github.com/python/cpython/blob/921f2353675ded86899c6dfdfc1c5c57bb998638/Lib/http/cookies.py#L408

werkzeug implements this logic like this: https://github.com/pallets/werkzeug/blob/3115aa6a6276939f5fd6efa46282e0256ff21f1a/src/werkzeug/http.py#L1228-L1230C29

Just some references.

@Kludex Kludex added this to the Version 0.23.0 milestone Dec 2, 2022
Comment thread starlette/responses.py Outdated
Comment thread tests/test_responses.py Outdated
@Kludex Kludex modified the milestones: Version 0.23.0, Version 0.24.0 Dec 8, 2022
@Kludex Kludex mentioned this pull request Dec 12, 2022
7 tasks
@Kludex Kludex force-pushed the issue-1878-fix-cookie-typing branch from f816a0a to 0433643 Compare December 17, 2022 09:57
@Kludex Kludex changed the title Issue 1878 fix cookie typing Support datetime on expires parameter on the set_cookie method Feb 4, 2023
Comment thread tests/test_responses.py
@Kludex Kludex requested a review from a team February 4, 2023 09:22
@Kludex Kludex force-pushed the issue-1878-fix-cookie-typing branch from a72d8ba to d5199ac Compare February 4, 2023 09:23
@Kludex

Kludex commented Feb 4, 2023

Copy link
Copy Markdown
Owner

If no one has anything against, I'm going to merge this when the release is ready.

@iudeen

iudeen commented Feb 4, 2023

Copy link
Copy Markdown
Contributor

Looks fine to me.

@Kludex Kludex changed the title Support datetime on expires parameter on the set_cookie method Support str and datetime on expires parameter on the set_cookie method Feb 4, 2023
@Kludex Kludex requested a review from adriangb February 4, 2023 17:47
Comment thread tests/test_responses.py
Comment thread tests/test_responses.py
@Kludex

Kludex commented Feb 5, 2023

Copy link
Copy Markdown
Owner

@florimondmanca I tried on this commit 6db2870

But it was flaky... Do you mean something different?

@florimondmanca

Copy link
Copy Markdown
Contributor

@Kludex I played with it locally. I feel like pytest's monkeypatch is just enough for our use case here, not really a need for a full-fledged extra library?

Diff:

diff --git a/requirements.txt b/requirements.txt
index 500a9b5..15aa541 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -9,7 +9,6 @@ flake8==3.9.2
 importlib-metadata==4.13.0
 isort==5.10.1
 mypy==0.991
-time-machine==2.9.0
 typing_extensions==4.4.0
 types-contextvars==2.4.7
 types-PyYAML==6.0.12.3
diff --git a/tests/test_responses.py b/tests/test_responses.py
index bf54772..aa2f5c4 100644
--- a/tests/test_responses.py
+++ b/tests/test_responses.py
@@ -1,6 +1,6 @@
 import os
-from datetime import datetime, timedelta, timezone
-from email.utils import format_datetime
+import datetime as dt
+import time
 from http.cookies import SimpleCookie
 
 import anyio
@@ -291,8 +291,10 @@ def test_file_response_with_inline_disposition(tmpdir, test_client_factory):
     assert response.headers["content-disposition"] == expected_disposition
 
 
-def test_set_cookie(test_client_factory, time_machine):
-    time_machine.move_to(datetime(2100, 1, 22, 12, 0, 0))
+def test_set_cookie(test_client_factory, monkeypatch):
+    # Mock time used as a reference for `Expires` by stdlib `SimpleCookie`.
+    mocked_now = dt.datetime(2100, 1, 22, 12, 0, 0, tzinfo=dt.timezone.utc)
+    monkeypatch.setattr(time, "time", lambda: mocked_now.timestamp())
 
     async def app(scope, receive, send):
         response = Response("Hello, world!", media_type="text/plain")
@@ -320,21 +322,23 @@ def test_set_cookie(test_client_factory, time_machine):
 
 
 @pytest.mark.parametrize(
-    "expires_factory",
+    "expires",
     [
-        lambda: datetime.now(timezone.utc) + timedelta(seconds=10),
-        lambda: format_datetime(
-            datetime.now(timezone.utc) + timedelta(seconds=10), usegmt=True
+        pytest.param(
+            dt.datetime(2100, 1, 22, 12, 0, 10, tzinfo=dt.timezone.utc), id="datetime"
         ),
-        lambda: 10,
+        pytest.param("Fri, 22 Jan 2100 12:00:10 GMT", id="str"),
+        pytest.param(10, id="int"),
     ],
 )
-def test_expires_on_set_cookie(test_client_factory, expires_factory, time_machine):
-    time_machine.move_to(datetime(2100, 1, 22, 12, 0, 0))
+def test_expires_on_set_cookie(test_client_factory, monkeypatch, expires):
+    # Mock time used as a reference for `Expires` by stdlib `SimpleCookie`.
+    mocked_now = dt.datetime(2100, 1, 22, 12, 0, 0, tzinfo=dt.timezone.utc)
+    monkeypatch.setattr(time, "time", lambda: mocked_now.timestamp())
 
     async def app(scope, receive, send):
         response = Response("Hello, world!", media_type="text/plain")
-        response.set_cookie("mycookie", "myvalue", expires=expires_factory())
+        response.set_cookie("mycookie", "myvalue", expires=expires)
         await response(scope, receive, send)
 
     client = test_client_factory(app)
@Kludex

Kludex commented Feb 6, 2023

Copy link
Copy Markdown
Owner

@Kludex I played with it locally. I feel like pytest's monkeypatch is just enough for our use case here, not really a need for a full-fledged extra library?

Diff:

diff --git a/requirements.txt b/requirements.txt
index 500a9b5..15aa541 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -9,7 +9,6 @@ flake8==3.9.2
 importlib-metadata==4.13.0
 isort==5.10.1
 mypy==0.991
-time-machine==2.9.0
 typing_extensions==4.4.0
 types-contextvars==2.4.7
 types-PyYAML==6.0.12.3
diff --git a/tests/test_responses.py b/tests/test_responses.py
index bf54772..aa2f5c4 100644
--- a/tests/test_responses.py
+++ b/tests/test_responses.py
@@ -1,6 +1,6 @@
 import os
-from datetime import datetime, timedelta, timezone
-from email.utils import format_datetime
+import datetime as dt
+import time
 from http.cookies import SimpleCookie
 
 import anyio
@@ -291,8 +291,10 @@ def test_file_response_with_inline_disposition(tmpdir, test_client_factory):
     assert response.headers["content-disposition"] == expected_disposition
 
 
-def test_set_cookie(test_client_factory, time_machine):
-    time_machine.move_to(datetime(2100, 1, 22, 12, 0, 0))
+def test_set_cookie(test_client_factory, monkeypatch):
+    # Mock time used as a reference for `Expires` by stdlib `SimpleCookie`.
+    mocked_now = dt.datetime(2100, 1, 22, 12, 0, 0, tzinfo=dt.timezone.utc)
+    monkeypatch.setattr(time, "time", lambda: mocked_now.timestamp())
 
     async def app(scope, receive, send):
         response = Response("Hello, world!", media_type="text/plain")
@@ -320,21 +322,23 @@ def test_set_cookie(test_client_factory, time_machine):
 
 
 @pytest.mark.parametrize(
-    "expires_factory",
+    "expires",
     [
-        lambda: datetime.now(timezone.utc) + timedelta(seconds=10),
-        lambda: format_datetime(
-            datetime.now(timezone.utc) + timedelta(seconds=10), usegmt=True
+        pytest.param(
+            dt.datetime(2100, 1, 22, 12, 0, 10, tzinfo=dt.timezone.utc), id="datetime"
         ),
-        lambda: 10,
+        pytest.param("Fri, 22 Jan 2100 12:00:10 GMT", id="str"),
+        pytest.param(10, id="int"),
     ],
 )
-def test_expires_on_set_cookie(test_client_factory, expires_factory, time_machine):
-    time_machine.move_to(datetime(2100, 1, 22, 12, 0, 0))
+def test_expires_on_set_cookie(test_client_factory, monkeypatch, expires):
+    # Mock time used as a reference for `Expires` by stdlib `SimpleCookie`.
+    mocked_now = dt.datetime(2100, 1, 22, 12, 0, 0, tzinfo=dt.timezone.utc)
+    monkeypatch.setattr(time, "time", lambda: mocked_now.timestamp())
 
     async def app(scope, receive, send):
         response = Response("Hello, world!", media_type="text/plain")
-        response.set_cookie("mycookie", "myvalue", expires=expires_factory())
+        response.set_cookie("mycookie", "myvalue", expires=expires)
         await response(scope, receive, send)
 
     client = test_client_factory(app)

I've applied this. Thanks!

@Kludex Kludex enabled auto-merge (squash) February 6, 2023 05:44
@Kludex Kludex merged commit 0a63a6e into Kludex:master Feb 6, 2023
aminalaee pushed a commit that referenced this pull request Feb 13, 2023
…e` 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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

5 participants