Skip to content

Commit 634489d

Browse files
committed
Type annotations for plain-loginlink
1 parent 8cdda13 commit 634489d

File tree

5 files changed

+64
-29
lines changed

5 files changed

+64
-29
lines changed

plain-loginlink/plain/loginlink/forms.py

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,24 @@
1+
from __future__ import annotations
2+
3+
from typing import TYPE_CHECKING, Any
4+
15
from plain import forms
26
from plain.auth import get_user_model
37
from plain.email import TemplateEmail
48

59
from .links import generate_link_url
610

11+
if TYPE_CHECKING:
12+
from plain.http import Request
13+
714

815
class LoginLinkForm(forms.Form):
916
email = forms.EmailField()
1017
next = forms.CharField(required=False)
1118

12-
def maybe_send_link(self, request, expires_in=60 * 60):
19+
def maybe_send_link(
20+
self, request: Request, expires_in: int = 60 * 60
21+
) -> int | None:
1322
user_model = get_user_model()
1423
email = self.cleaned_data["email"]
1524
try:
@@ -31,7 +40,11 @@ def maybe_send_link(self, request, expires_in=60 * 60):
3140
)
3241
return email.send()
3342

34-
def get_template_email(self, *, email, context):
43+
return None
44+
45+
def get_template_email(
46+
self, *, email: str, context: dict[str, Any]
47+
) -> TemplateEmail:
3548
return TemplateEmail(
3649
template="loginlink",
3750
to=[email],

plain-loginlink/plain/loginlink/links.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,17 @@
1+
from __future__ import annotations
2+
3+
from typing import TYPE_CHECKING
4+
15
from plain.auth import get_user_model
26
from plain.signing import BadSignature, SignatureExpired
37
from plain.urls import reverse
48

59
from . import signing
610

11+
if TYPE_CHECKING:
12+
from plain.http import Request
13+
from plain.models import Model
14+
715

816
class LoginLinkExpired(Exception):
917
pass
@@ -17,7 +25,9 @@ class LoginLinkChanged(Exception):
1725
pass
1826

1927

20-
def generate_link_url(*, request, user, email, expires_in):
28+
def generate_link_url(
29+
*, request: Request, user: Model, email: str, expires_in: int
30+
) -> str:
2131
"""
2232
Generate a login link using both the user's ID
2333
and email address, so links break if the user email changes or is assigned to another user.
@@ -27,7 +37,7 @@ def generate_link_url(*, request, user, email, expires_in):
2737
return request.build_absolute_uri(reverse("loginlink:login", token))
2838

2939

30-
def get_link_token_user(token):
40+
def get_link_token_user(token: str) -> Model:
3141
"""
3242
Validate a link token and get the user from it.
3343
"""

plain-loginlink/plain/loginlink/signing.py

Lines changed: 25 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
1+
from __future__ import annotations
2+
13
import time
24
import zlib
5+
from typing import Any
36

47
from plain.signing import (
58
JSONSerializer,
@@ -15,12 +18,12 @@
1518
class ExpiringSigner(Signer):
1619
"""A signer with an embedded expiration (vs max age unsign)"""
1720

18-
def sign(self, value, expires_in):
21+
def sign(self, value: str, expires_in: int) -> str:
1922
timestamp = b62_encode(int(time.time() + expires_in))
2023
value = f"{value}{self.sep}{timestamp}"
2124
return super().sign(value)
2225

23-
def unsign(self, value):
26+
def unsign(self, value: str) -> str:
2427
"""
2528
Retrieve original value and check the expiration hasn't passed.
2629
"""
@@ -32,8 +35,12 @@ def unsign(self, value):
3235
return value
3336

3437
def sign_object(
35-
self, obj, serializer=JSONSerializer, compress=False, expires_in=None
36-
):
38+
self,
39+
obj: Any,
40+
serializer: type = JSONSerializer,
41+
compress: bool = False,
42+
expires_in: int | None = None,
43+
) -> str:
3744
"""
3845
Return URL-safe, hmac signed base64 compressed JSON string.
3946
@@ -58,7 +65,7 @@ def sign_object(
5865
base64d = "." + base64d
5966
return self.sign(base64d, expires_in)
6067

61-
def unsign_object(self, signed_obj, serializer=JSONSerializer):
68+
def unsign_object(self, signed_obj: str, serializer: type = JSONSerializer) -> Any:
6269
# Signer.unsign() returns str but base64 and zlib compression operate
6370
# on bytes.
6471
base64d = self.unsign(signed_obj).encode()
@@ -73,13 +80,13 @@ def unsign_object(self, signed_obj, serializer=JSONSerializer):
7380

7481

7582
def dumps(
76-
obj,
77-
key=None,
78-
salt="plain.loginlink",
79-
serializer=JSONSerializer,
80-
compress=False,
81-
expires_in=None,
82-
):
83+
obj: Any,
84+
key: str | None = None,
85+
salt: str = "plain.loginlink",
86+
serializer: type = JSONSerializer,
87+
compress: bool = False,
88+
expires_in: int | None = None,
89+
) -> str:
8390
"""
8491
Return URL-safe, hmac signed base64 compressed JSON string. If key is
8592
None, use settings.SECRET_KEY instead. The hmac algorithm is the default
@@ -102,12 +109,12 @@ def dumps(
102109

103110

104111
def loads(
105-
s,
106-
key=None,
107-
salt="plain.loginlink",
108-
serializer=JSONSerializer,
109-
fallback_keys=None,
110-
):
112+
s: str,
113+
key: str | None = None,
114+
salt: str = "plain.loginlink",
115+
serializer: type = JSONSerializer,
116+
fallback_keys: list[str] | None = None,
117+
) -> Any:
111118
"""
112119
Reverse of dumps(), raise BadSignature if signature fails.
113120

plain-loginlink/plain/loginlink/views.py

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
1+
from __future__ import annotations
2+
3+
from typing import Any
4+
15
from plain.auth import login, logout
26
from plain.auth.views import AuthViewMixin
3-
from plain.http import ResponseRedirect
7+
from plain.http import Response, ResponseRedirect
48
from plain.runtime import settings
59
from plain.urls import reverse, reverse_lazy
610
from plain.views import FormView, TemplateView, View
@@ -18,19 +22,19 @@ class LoginLinkFormView(AuthViewMixin, FormView):
1822
form_class = LoginLinkForm
1923
success_url = reverse_lazy("loginlink:sent")
2024

21-
def get(self):
25+
def get(self) -> Response:
2226
# Redirect if the user is already logged in
2327
if self.user:
2428
form = self.get_form()
2529
return ResponseRedirect(self.get_success_url(form))
2630

2731
return super().get()
2832

29-
def form_valid(self, form):
33+
def form_valid(self, form: LoginLinkForm) -> Response:
3034
form.maybe_send_link(self.request)
3135
return super().form_valid(form)
3236

33-
def get_success_url(self, form):
37+
def get_success_url(self, form: LoginLinkForm) -> str:
3438
if next_url := form.cleaned_data.get("next"):
3539
# Keep the next URL in the query string so the sent
3640
# view can redirect to it if reloaded and logged in already.
@@ -42,7 +46,7 @@ def get_success_url(self, form):
4246
class LoginLinkSentView(AuthViewMixin, TemplateView):
4347
template_name = "loginlink/sent.html"
4448

45-
def get(self):
49+
def get(self) -> Response:
4650
# Redirect if the user is already logged in
4751
if self.user:
4852
next_url = self.request.query_params.get("next", "/")
@@ -54,7 +58,7 @@ def get(self):
5458
class LoginLinkFailedView(TemplateView):
5559
template_name = "loginlink/failed.html"
5660

57-
def get_template_context(self):
61+
def get_template_context(self) -> dict[str, Any]:
5862
context = super().get_template_context()
5963
context["error"] = self.request.query_params.get("error")
6064
context["login_url"] = reverse(settings.AUTH_LOGIN_URL)
@@ -64,7 +68,7 @@ def get_template_context(self):
6468
class LoginLinkLoginView(AuthViewMixin, View):
6569
success_url = "/"
6670

67-
def get(self):
71+
def get(self) -> Response:
6872
# If they're logged in, log them out and process the link again
6973
if self.user:
7074
logout(self.request)

scripts/type-validate

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ FULLY_TYPED_DIRS = [
2525
"plain-esbuild/plain/esbuild",
2626
"plain-flags/plain/flags",
2727
"plain-htmx/plain/htmx",
28+
"plain-loginlink/plain/loginlink",
2829
"plain-sessions/plain/sessions",
2930
]
3031

0 commit comments

Comments
 (0)