Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 23 commits into from
Feb 6, 2023

Conversation

oskipa
Copy link
Contributor

@oskipa oskipa commented Oct 8, 2022

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.

starlette/responses.py Outdated Show resolved Hide resolved
starlette/responses.py Outdated Show resolved Hide resolved
starlette/responses.py Outdated Show resolved Hide resolved
starlette/responses.py Outdated Show resolved Hide resolved
@Kludex Kludex mentioned this pull request Oct 12, 2022
Copy link
Member

@Kludex Kludex left a comment

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.

starlette/responses.py Outdated Show resolved Hide resolved
starlette/responses.py Outdated Show resolved Hide resolved
tests/test_responses.py Outdated Show resolved Hide resolved
@oskipa
Copy link
Contributor Author

oskipa commented Oct 20, 2022

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
Copy link
Contributor Author

oskipa commented Oct 21, 2022

The tests are passing now :)

@Kludex
Copy link
Member

Kludex commented Oct 22, 2022

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
Copy link
Contributor Author

oskipa commented Oct 25, 2022

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
Copy link
Contributor Author

oskipa commented Oct 25, 2022

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
Copy link
Contributor Author

oskipa commented Nov 7, 2022

Is there anything else to do?

@Kludex
Copy link
Member

Kludex commented Nov 10, 2022

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
starlette/responses.py Outdated Show resolved Hide resolved
tests/test_responses.py Outdated Show resolved Hide resolved
@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
tests/test_responses.py Outdated Show resolved Hide resolved
@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
Copy link
Member

Kludex commented Feb 4, 2023

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

@iudeen
Copy link

iudeen commented Feb 4, 2023

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
tests/test_responses.py Show resolved Hide resolved
tests/test_responses.py Outdated Show resolved Hide resolved
@Kludex
Copy link
Member

Kludex commented Feb 5, 2023

@florimondmanca I tried on this commit 6db2870

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

@florimondmanca
Copy link
Member

@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
Copy link
Member

Kludex commented Feb 6, 2023

@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 encode: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
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Wrong typing for expires in set_cookie
5 participants