Fix type errors (#81)

This brings down the number of type errors from 118 down to 92. I think most of the remaining errors are due to the */models.py files where I'm not sure what to do about it. I run the type checker in docker via `just typecheck`.

The change in 0af3fbcac4 requires this change upstream: https://github.com/valberg/django-registries/pull/28 we can omit that commit for now.

Reviewed-on: https://git.data.coop/data.coop/membersystem/pulls/81
Reviewed-by: benjaoming <benjaoming@data.coop>
Co-authored-by: Reynir Björnsson <reynir@reynir.dk>
Co-committed-by: Reynir Björnsson <reynir@reynir.dk>
This commit is contained in:
Reynir Björnsson 2025-03-03 18:20:28 +00:00 committed by valberg
parent 408970f16e
commit c9b0c19fec
10 changed files with 34 additions and 31 deletions

View file

@ -33,12 +33,11 @@ class OrderAdminForm(forms.ModelForm):
def clean(self) -> None: def clean(self) -> None:
"""Clean the order.""" """Clean the order."""
cd = super().clean() cd = super().clean()
if not cd["account"] and cd["member"]: if cd and not cd["account"] and cd["member"]:
try: try:
cd["account"] = models.Account.objects.get_or_create(owner=cd["member"])[0] cd["account"] = models.Account.objects.get_or_create(owner=cd["member"])[0]
except models.Account.MultipleObjectsReturned: except models.Account.MultipleObjectsReturned:
cd["account"] = models.Account.objects.filter(owner=cd["member"]).first() cd["account"] = models.Account.objects.filter(owner=cd["member"]).first()
return cd
@admin.register(models.Order) @admin.register(models.Order)

View file

@ -85,6 +85,7 @@ def order_pay(request: HttpRequest, order_id: int) -> HttpResponse:
# TODO(benjaoming): Redirect with status=303 # TODO(benjaoming): Redirect with status=303
# https://git.data.coop/data.coop/membersystem/issues/63 # https://git.data.coop/data.coop/membersystem/issues/63
# TODO(reynir): url is None if session is not active
return redirect(checkout_session.url) return redirect(checkout_session.url)

View file

@ -20,6 +20,7 @@ from django.utils.translation import gettext_lazy as _
if TYPE_CHECKING: if TYPE_CHECKING:
from accounting.models import Order from accounting.models import Order
from django.http import HttpRequest from django.http import HttpRequest
from django_stubs_ext import StrOrPromise
from .models import Membership from .models import Membership
@ -35,7 +36,7 @@ class BaseEmail(EmailMessage):
template = "membership/email/base.txt" template = "membership/email/base.txt"
# Optional: Set to a template path for subject # Optional: Set to a template path for subject
template_subject = None template_subject = None
default_subject = "SET SUBJECT HERE" default_subject : StrOrPromise = "SET SUBJECT HERE"
def __init__(self, request: HttpRequest, *args, **kwargs) -> None: # noqa: ANN002, ANN003 def __init__(self, request: HttpRequest, *args, **kwargs) -> None: # noqa: ANN002, ANN003
self.context = kwargs.pop("context", {}) self.context = kwargs.pop("context", {})
@ -90,7 +91,7 @@ class BaseEmail(EmailMessage):
def send_with_feedback(self, *, success_msg: str | None = None, no_message: bool = False) -> None: def send_with_feedback(self, *, success_msg: str | None = None, no_message: bool = False) -> None:
"""Send email, possibly adding feedback via django.contrib.messages.""" """Send email, possibly adding feedback via django.contrib.messages."""
if not success_msg: if success_msg is None:
success_msg = _("Email successfully sent to {}").format(", ".join(self.to)) success_msg = _("Email successfully sent to {}").format(", ".join(self.to))
try: try:
self.send(fail_silently=False) self.send(fail_silently=False)

View file

@ -2,13 +2,17 @@
from __future__ import annotations from __future__ import annotations
from typing import TYPE_CHECKING
from dataclasses import dataclass from dataclasses import dataclass
from django.contrib.auth.models import Permission as DjangoPermission from django.contrib.auth.models import Permission as DjangoPermission
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
PERMISSIONS = [] if TYPE_CHECKING:
from django_stubs_ext import StrOrPromise
PERMISSIONS : list[Permission] = []
def persist_permissions(*args, **kwargs) -> None: # type: ignore[no-untyped-def] # noqa: ANN002, ANN003 def persist_permissions(*args, **kwargs) -> None: # type: ignore[no-untyped-def] # noqa: ANN002, ANN003
@ -21,7 +25,7 @@ def persist_permissions(*args, **kwargs) -> None: # type: ignore[no-untyped-def
class Permission: class Permission:
"""Dataclass to define a permission.""" """Dataclass to define a permission."""
name: str name: StrOrPromise
codename: str codename: str
app_label: str app_label: str
model: str model: str

View file

@ -25,6 +25,7 @@ from .selectors import get_memberships
from .selectors import get_subscription_periods from .selectors import get_subscription_periods
if TYPE_CHECKING: if TYPE_CHECKING:
from utils.types import AuthenticatedHttpRequest
from django.http import HttpRequest from django.http import HttpRequest
from django.http import HttpResponse from django.http import HttpResponse
@ -36,7 +37,7 @@ member_view = namespaced_decorator_factory(namespace="member", base_path="member
name="membership-overview", name="membership-overview",
login_required=True, login_required=True,
) )
def membership_overview(request: HttpRequest) -> HttpResponse: def membership_overview(request: AuthenticatedHttpRequest) -> HttpResponse:
"""View to show the membership overview.""" """View to show the membership overview."""
memberships = get_memberships(member=request.user) memberships = get_memberships(member=request.user)
current_membership = memberships.current() current_membership = memberships.current()
@ -69,7 +70,7 @@ admin_members_view = namespaced_decorator_factory(
login_required=True, login_required=True,
permissions=[ADMINISTRATE_MEMBERS.path], permissions=[ADMINISTRATE_MEMBERS.path],
) )
def members_admin(request: HttpRequest) -> HttpResponse: def members_admin(request: AuthenticatedHttpRequest) -> HttpResponse:
"""View to list all members.""" """View to list all members."""
users = get_members() users = get_members()
@ -105,7 +106,7 @@ def members_admin(request: HttpRequest) -> HttpResponse:
login_required=True, login_required=True,
permissions=[ADMINISTRATE_MEMBERS.path], permissions=[ADMINISTRATE_MEMBERS.path],
) )
def members_admin_detail(request: HttpRequest, member_id: int) -> HttpResponse: def members_admin_detail(request: AuthenticatedHttpRequest, member_id: int) -> HttpResponse:
"""View to show the details of a member.""" """View to show the details of a member."""
member = get_member(member_id=member_id) member = get_member(member_id=member_id)
subscription_periods = get_subscription_periods(member=member) subscription_periods = get_subscription_periods(member=member)
@ -123,12 +124,6 @@ def members_admin_detail(request: HttpRequest, member_id: int) -> HttpResponse:
) )
class InvalidTokenError(HttpResponseForbidden):
"""Exception raised when an invalid token is encountered."""
content = "Token not valid - maybe it expired?"
@ratelimit(group="membership", key="ip", rate="10/d", method="ALL", block=True) @ratelimit(group="membership", key="ip", rate="10/d", method="ALL", block=True)
@member_view( @member_view(
paths="invite/<str:referral_code>/<str:token>/", paths="invite/<str:referral_code>/<str:token>/",
@ -152,7 +147,7 @@ def invite(request: HttpRequest, referral_code: str, token: str) -> HttpResponse
token_valid = default_token_generator.check_token(membership.user, token) token_valid = default_token_generator.check_token(membership.user, token)
if not token_valid: if not token_valid:
raise InvalidTokenError return HttpResponseForbidden("Token not valid - maybe it expired?")
if request.method == "POST": if request.method == "POST":
form = InviteForm(membership=membership, data=request.POST) form = InviteForm(membership=membership, data=request.POST)

View file

@ -108,7 +108,7 @@ AUTHENTICATION_BACKENDS = (
WSGI_APPLICATION = "project.wsgi.application" WSGI_APPLICATION = "project.wsgi.application"
AUTH_PASSWORD_VALIDATORS = [] AUTH_PASSWORD_VALIDATORS : list[dict[str, str]] = []
LANGUAGE_CODE = "da-dk" LANGUAGE_CODE = "da-dk"

View file

@ -7,7 +7,7 @@ from django_registries.registry import Interface
from django_registries.registry import Registry from django_registries.registry import Registry
class ServiceRegistry(Registry): class ServiceRegistry(Registry["ServiceInterface"]):
"""Registry for services.""" """Registry for services."""
implementations_module = "services" implementations_module = "services"
@ -39,9 +39,9 @@ class ServiceInterface(Interface):
# This is a way of saying that the service is "mandatory" to have. # This is a way of saying that the service is "mandatory" to have.
auto_create: bool = False auto_create: bool = False
request_types: list[str, str] = DEFAULT_SERVICE_REQUEST_TYPES request_types: list[ServiceRequests] = DEFAULT_SERVICE_REQUEST_TYPES
subscribe_fields: tuple[tuple[str, forms.Field]] = [] subscribe_fields: tuple[tuple[str, forms.Field],...] = ()
def get_form_class(self) -> type: def get_form_class(self) -> type:
"""Get the form class for the service.""" """Get the form class for the service."""

View file

@ -14,7 +14,7 @@ from services.registry import ServiceRegistry
if TYPE_CHECKING: if TYPE_CHECKING:
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.http import HttpRequest from utils.types import AuthenticatedHttpRequest
from django.http import HttpResponse from django.http import HttpResponse
services_view = namespaced_decorator_factory( services_view = namespaced_decorator_factory(
@ -28,16 +28,16 @@ services_view = namespaced_decorator_factory(
name="list", name="list",
login_required=True, login_required=True,
) )
def services_overview(request: HttpRequest) -> HttpResponse: def services_overview(request: AuthenticatedHttpRequest) -> HttpResponse:
"""View all services.""" """View all services."""
active_services = get_services(user=request.user) active_services = get_services(user=request.user)
active_service_classes = [service.__class__ for service in active_services] active_service_classes = [service.__class__ for service in active_services]
services = [service for _, service in ServiceRegistry.get_items() if service not in active_service_classes] non_active_services = [service for _, service in ServiceRegistry.get_items() if service not in active_service_classes]
context = { context = {
"non_active_services": services, "non_active_services": non_active_services,
"active_services": active_services, "active_services": active_services,
} }
@ -53,7 +53,7 @@ def services_overview(request: HttpRequest) -> HttpResponse:
name="detail", name="detail",
login_required=True, login_required=True,
) )
def service_detail(request: HttpRequest, service_slug: str) -> HttpResponse: def service_detail(request: AuthenticatedHttpRequest, service_slug: str) -> HttpResponse:
"""View a service.""" """View a service."""
service = ServiceRegistry.get(slug=service_slug) service = ServiceRegistry.get(slug=service_slug)
@ -73,7 +73,7 @@ def service_detail(request: HttpRequest, service_slug: str) -> HttpResponse:
name="subscribe", name="subscribe",
login_required=True, login_required=True,
) )
def service_subscribe(request: HttpRequest, service_slug: str) -> HttpResponse: def service_subscribe(request: AuthenticatedHttpRequest, service_slug: str) -> HttpResponse:
"""Subscribe to a service.""" """Subscribe to a service."""
service = ServiceRegistry.get(slug=service_slug) service = ServiceRegistry.get(slug=service_slug)

View file

@ -7,4 +7,5 @@ from django.http import HttpRequest
class AuthenticatedHttpRequest(HttpRequest): class AuthenticatedHttpRequest(HttpRequest):
"""HttpRequest with an authenticated user.""" """HttpRequest with an authenticated user."""
# XXX(reynir): Should this be Member instead?!
user: User user: User

View file

@ -19,6 +19,7 @@ if TYPE_CHECKING:
from django.db.models import QuerySet from django.db.models import QuerySet
from django.http import HttpRequest from django.http import HttpRequest
from django.http import HttpResponse from django.http import HttpResponse
from django_stubs_ext import StrOrPromise
@dataclass @dataclass
@ -26,18 +27,18 @@ class Row:
"""A row in a table.""" """A row in a table."""
data: dict[str, str] data: dict[str, str]
actions: list[dict[str, str]] actions: list[dict[str, StrOrPromise]]
@dataclass @dataclass
class RowAction: class RowAction:
"""An action that can be performed on a row in a table.""" """An action that can be performed on a row in a table."""
label: str label: StrOrPromise
url_name: str url_name: str
url_kwargs: dict[str, str] url_kwargs: dict[str, str]
def render(self, obj: Model) -> dict[str, str]: def render(self, obj: Model) -> dict[str, StrOrPromise]:
"""Render the action as a dictionary for the given object.""" """Render the action as a dictionary for the given object."""
url = reverse( url = reverse(
self.url_name, self.url_name,
@ -53,7 +54,7 @@ class RenderConfig:
entity_name: str entity_name: str
entity_name_plural: str entity_name_plural: str
objects: QuerySet objects: QuerySet
columns: list[tuple[str, str]] columns: list[tuple[str, StrOrPromise]]
row_actions: list[RowAction] | None = None row_actions: list[RowAction] | None = None
list_actions: list[tuple[str, str]] | None = None list_actions: list[tuple[str, str]] | None = None
paginate_by: int | None = None paginate_by: int | None = None
@ -88,7 +89,8 @@ class RenderConfig:
for obj in objects: for obj in objects:
with queries_disabled(): with queries_disabled():
row = Row( row = Row(
data={column: getattr(obj, column[0]) for column in columns}, # XXX(reynir): we never use the key
data={column[0]: getattr(obj, column[0]) for column in columns},
actions=[action.render(obj) for action in row_actions], actions=[action.render(obj) for action in row_actions],
) )
rows.append(row) rows.append(row)