From 5e627d4c47bf49fdb105ea5c735fab07cf0cdfd6 Mon Sep 17 00:00:00 2001 From: JasonGrace2282 Date: Wed, 12 Jun 2024 19:21:38 -0400 Subject: [PATCH] chore: update typehints via __future__.annotations This commit changes all typehints from "old" ways of typehinting (type comments, `Union`, etc.) to more modern ways, like using | notation for unions, etc. --- .../management/commands/import_students.py | 4 +- .../apps/eighth/forms/admin/activities.py | 5 +- intranet/apps/eighth/models.py | 81 ++++++++++--------- intranet/apps/eighth/tasks.py | 2 + intranet/apps/eighth/views/admin/groups.py | 9 ++- intranet/apps/eighth/views/admin/hybrid.py | 5 +- intranet/apps/features/helpers.py | 4 +- intranet/apps/notifications/emails.py | 10 ++- intranet/apps/printing/views.py | 16 ++-- intranet/apps/signage/views.py | 5 +- intranet/apps/templatetags/paginate.py | 6 +- intranet/apps/users/models.py | 53 ++++++------ intranet/settings/__init__.py | 32 ++++---- intranet/utils/helpers.py | 8 +- intranet/utils/html.py | 16 ++-- intranet/utils/locking.py | 12 +-- pyproject.toml | 2 - 17 files changed, 146 insertions(+), 124 deletions(-) diff --git a/intranet/apps/dataimport/management/commands/import_students.py b/intranet/apps/dataimport/management/commands/import_students.py index 45c37ce2b5e..c42d82911a9 100644 --- a/intranet/apps/dataimport/management/commands/import_students.py +++ b/intranet/apps/dataimport/management/commands/import_students.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import csv import sys @@ -54,7 +56,7 @@ def generate_single_username( return f"{graduating_year}{first_stripped[0]}{last_stripped[:7]}".lower() - def find_next_available_username(self, used_username: str, username_set: set = None) -> str: + def find_next_available_username(self, used_username: str, username_set: set[str] | None = None) -> str: """Find the next available username. Args: diff --git a/intranet/apps/eighth/forms/admin/activities.py b/intranet/apps/eighth/forms/admin/activities.py index 0f4d8953252..1731e71d4ea 100644 --- a/intranet/apps/eighth/forms/admin/activities.py +++ b/intranet/apps/eighth/forms/admin/activities.py @@ -1,5 +1,6 @@ +from __future__ import annotations + import logging -from typing import List # noqa from django import forms, http from django.contrib.auth import get_user_model @@ -11,7 +12,7 @@ class ActivityDisplayField(forms.ModelChoiceField): - cancelled_acts = None # type: List[EighthActivity] + cancelled_acts: list[EighthActivity] | None = None def __init__(self, *args, **kwargs): if "block" in kwargs: diff --git a/intranet/apps/eighth/models.py b/intranet/apps/eighth/models.py index 6fca981ebe8..c1ff7b253d0 100644 --- a/intranet/apps/eighth/models.py +++ b/intranet/apps/eighth/models.py @@ -1,12 +1,15 @@ # pylint: disable=too-many-lines; Allow more than 1000 lines +from __future__ import annotations + import datetime import logging import string -from typing import Collection, Iterable, List, Optional, Union +from typing import Iterable from cacheops import invalidate_obj from django.conf import settings from django.contrib.auth import get_user_model +from django.contrib.auth.models import AbstractBaseUser from django.contrib.auth.models import Group as DjangoGroup from django.core import validators from django.core.cache import cache @@ -127,7 +130,7 @@ class EighthRoom(AbstractBaseEighthModel): unique_together = (("name", "capacity"),) @staticmethod - def total_capacity_of_rooms(rooms: Iterable["EighthRoom"]) -> int: + def total_capacity_of_rooms(rooms: Iterable[EighthRoom]) -> int: """Returns the total capacity of the provided rooms. Args: rooms: Rooms to determine total capacity for. @@ -313,7 +316,7 @@ def name_with_flags_no_restricted(self) -> str: """ return self._name_with_flags(False) - def _name_with_flags(self, include_restricted: bool, title: Optional[str] = None) -> str: + def _name_with_flags(self, include_restricted: bool, title: str | None = None) -> str: """Generates the activity's name with flags. Args: include_restricted: Whether to include the "restricted" flag. @@ -334,7 +337,7 @@ def _name_with_flags(self, include_restricted: bool, title: Optional[str] = None return name @classmethod - def restricted_activities_available_to_user(cls, user: "get_user_model()") -> List[int]: + def restricted_activities_available_to_user(cls, user: AbstractBaseUser) -> list[int]: """Finds the restricted activities available to the given user. Args: user: The User to find the restricted activities for. @@ -352,7 +355,7 @@ def restricted_activities_available_to_user(cls, user: "get_user_model()") -> Li return EighthActivity.objects.filter(q).values_list("id", flat=True) @classmethod - def available_ids(cls) -> List[int]: + def available_ids(cls) -> list[int]: """Returns all available IDs not used by an EighthActivity. Returns: A list of the available activity IDs. @@ -364,7 +367,7 @@ def available_ids(cls) -> List[int]: avail = nums - used return list(avail) - def get_active_schedulings(self) -> Union[QuerySet, Collection["EighthScheduledActivity"]]: # pylint: disable=unsubscriptable-object + def get_active_schedulings(self) -> QuerySet[EighthScheduledActivity]: # pylint: disable=unsubscriptable-object """Returns all EighthScheduledActivitys scheduled this year for this activity. Returns: EighthScheduledActivitys of this activity occurring this year. @@ -383,7 +386,7 @@ def is_active(self) -> bool: return self.get_active_schedulings().exists() @property - def frequent_users(self) -> Union[QuerySet, Collection["get_user_model()"]]: # pylint: disable=unsubscriptable-object + def frequent_users(self) -> QuerySet[AbstractBaseUser]: # pylint: disable=unsubscriptable-object """Return a QuerySet of user id's and counts that have signed up for this activity more than `settings.SIMILAR_THRESHOLD` times. This is used for suggesting activities to users. @@ -420,7 +423,7 @@ def __str__(self): class EighthBlockQuerySet(models.query.QuerySet): - def this_year(self) -> Union[QuerySet, Collection["EighthBlock"]]: # pylint: disable=unsubscriptable-object + def this_year(self) -> QuerySet[EighthBlock]: # pylint: disable=unsubscriptable-object """Get EighthBlocks from this school year only. Returns: A QuerySet containing all of the blocks selected by this QuerySet that occur during this school year. @@ -428,7 +431,7 @@ def this_year(self) -> Union[QuerySet, Collection["EighthBlock"]]: # pylint: di start_date, end_date = get_date_range_this_year() return self.filter(date__gte=start_date, date__lte=end_date) - def filter_today(self) -> Union[QuerySet, Collection["EighthBlock"]]: # pylint: disable=unsubscriptable-object + def filter_today(self) -> QuerySet[EighthBlock]: # pylint: disable=unsubscriptable-object """Gets EighthBlocks that occur today. Returns: A QuerySet containing all of the blocks selected by this QuerySet that occur today. @@ -440,7 +443,7 @@ class EighthBlockManager(models.Manager): def get_queryset(self): return EighthBlockQuerySet(self.model, using=self._db) - def get_upcoming_blocks(self, max_number: int = -1) -> Union[QuerySet, Collection["EighthBlock"]]: # pylint: disable=unsubscriptable-object + def get_upcoming_blocks(self, max_number: int = -1) -> QuerySet[EighthBlock]: # pylint: disable=unsubscriptable-object """Gets the given number of upcoming blocks that will take place in the future. If there is no block in the future, the most recent block will be returned. Returns: @@ -460,7 +463,7 @@ def get_upcoming_blocks(self, max_number: int = -1) -> Union[QuerySet, Collectio return blocks[:max_number] - def get_first_upcoming_block(self) -> "EighthBlock": + def get_first_upcoming_block(self) -> EighthBlock: """Gets the first upcoming block (the first block that will take place in the future). Returns: The first upcoming ``EighthBlock`` object, or ``None`` if there are none upcoming. @@ -468,7 +471,7 @@ def get_first_upcoming_block(self) -> "EighthBlock": return self.get_upcoming_blocks().first() - def get_next_upcoming_blocks(self) -> Union[QuerySet, Collection["EighthBlock"]]: # pylint: disable=unsubscriptable-object + def get_next_upcoming_blocks(self) -> QuerySet[EighthBlock]: # pylint: disable=unsubscriptable-object """Gets the next upccoming blocks. It finds the other blocks that are occurring on the day of the first upcoming block. @@ -484,7 +487,7 @@ def get_next_upcoming_blocks(self) -> Union[QuerySet, Collection["EighthBlock"]] next_blocks = EighthBlock.objects.filter(date=next_block.date) return next_blocks - def get_blocks_this_year(self) -> Union[QuerySet, Collection["EighthBlock"]]: # pylint: disable=unsubscriptable-object + def get_blocks_this_year(self) -> QuerySet[EighthBlock]: # pylint: disable=unsubscriptable-object """Gets a QuerySet of blocks that occur this school year. Returns: A QuerySet of all the blocks that occur during this school year. @@ -494,7 +497,7 @@ def get_blocks_this_year(self) -> Union[QuerySet, Collection["EighthBlock"]]: # return EighthBlock.objects.filter(date__gte=date_start, date__lte=date_end) - def get_blocks_today(self) -> Union[QuerySet, Collection["EighthBlock"]]: # pylint: disable=unsubscriptable-object + def get_blocks_today(self) -> QuerySet[EighthBlock]: # pylint: disable=unsubscriptable-object """Gets a QuerySet of blocks that occur today. Returns: A QuerySet of all the blocks that occur today. @@ -518,9 +521,9 @@ class EighthBlock(AbstractBaseEighthModel): locked Whether signups are closed. activities - List of :class:`EighthScheduledActivity`\s for the block. + list of :class:`EighthScheduledActivity`\s for the block. override_blocks - List of :class:`EighthBlock`\s that the block overrides. + list of :class:`EighthBlock`\s that the block overrides. This allows the half-blocks used during Techlab visits to be easily managed. If a student should only be allowed to sign up for either only block A or both blocks A1 and A2, then block A @@ -551,7 +554,7 @@ def save(self, *args, **kwargs): # pylint: disable=signature-differs super().save(*args, **kwargs) - def next_blocks(self, quantity: int = -1) -> Union[QuerySet, Collection["EighthBlock"]]: # pylint: disable=unsubscriptable-object + def next_blocks(self, quantity: int = -1) -> QuerySet[EighthBlock]: # pylint: disable=unsubscriptable-object """Gets future blocks this school year in order. Args: quantity: The number of blocks to list after this block, or -1 for all following blocks. @@ -568,7 +571,7 @@ def next_blocks(self, quantity: int = -1) -> Union[QuerySet, Collection["EighthB return blocks return blocks[:quantity] - def previous_blocks(self, quantity: int = -1) -> Union[QuerySet, Collection["EighthBlock"]]: # pylint: disable=unsubscriptable-object + def previous_blocks(self, quantity: int = -1) -> QuerySet[EighthBlock]: # pylint: disable=unsubscriptable-object """Gets the previous blocks this school year in order. Args: quantity: The number of blocks to list before this block, or -1 for all previous blocks. @@ -647,7 +650,7 @@ def num_no_signups(self) -> int: signup_users_count = get_user_model().objects.get_students().count() return signup_users_count - self.num_signups() - def get_unsigned_students(self) -> Union[QuerySet, Collection["get_user_model()"]]: # pylint: disable=unsubscriptable-object + def get_unsigned_students(self) -> QuerySet[AbstractBaseUser]: # pylint: disable=unsubscriptable-object """Return a QuerySet of people who haven't signed up for an activity during this block. Returns: @@ -655,7 +658,7 @@ def get_unsigned_students(self) -> Union[QuerySet, Collection["get_user_model()" """ return get_user_model().objects.get_students().exclude(eighthsignup__scheduled_activity__block=self) - def get_hidden_signups(self) -> Union[QuerySet, Collection["EighthSignup"]]: # pylint: disable=unsubscriptable-object + def get_hidden_signups(self) -> QuerySet[EighthSignup]: # pylint: disable=unsubscriptable-object """Returns a QuerySet of EighthSignups whose users are not students but have signed up for an activity. This is usually a list of signups for the z-Withdrawn from TJ activity. @@ -732,7 +735,7 @@ class Meta: class EighthScheduledActivityManager(Manager): """Model Manager for EighthScheduledActivity.""" - def for_sponsor(self, sponsor: EighthSponsor, include_cancelled: bool = False) -> Union[QuerySet, Collection["EighthScheduledActivity"]]: # pylint: disable=unsubscriptable-object + def for_sponsor(self, sponsor: EighthSponsor, include_cancelled: bool = False) -> QuerySet[EighthScheduledActivity]: # pylint: disable=unsubscriptable-object """Returns a QuerySet of EighthScheduledActivities where the given EighthSponsor is sponsoring. If a sponsorship is defined in an EighthActivity, it may be overridden @@ -820,7 +823,7 @@ class EighthScheduledActivity(AbstractBaseEighthModel): history = HistoricalRecords() - def get_all_associated_rooms(self) -> Union[QuerySet, Collection["EighthRoom"]]: # pylint: disable=unsubscriptable-object + def get_all_associated_rooms(self) -> QuerySet[EighthRoom]: # pylint: disable=unsubscriptable-object """Returns a QuerySet of all the rooms associated with either this EighthScheduledActivity or its EighthActivity. Returns: A QuerySet of all the rooms associated with either this EighthScheduledActivity or its EighthActivity. @@ -853,7 +856,7 @@ def title_with_flags(self) -> str: name_with_flags = "Special: " + name_with_flags return name_with_flags - def get_true_sponsors(self) -> Union[QuerySet, Collection[EighthSponsor]]: # pylint: disable=unsubscriptable-object + def get_true_sponsors(self) -> QuerySet[EighthSponsor]: # pylint: disable=unsubscriptable-object """Retrieves the sponsors for the scheduled activity, taking into account activity defaults and overrides. Returns: @@ -861,7 +864,7 @@ def get_true_sponsors(self) -> Union[QuerySet, Collection[EighthSponsor]]: # py """ return self.sponsors.all() or self.activity.sponsors.all() - def user_is_sponsor(self, user: "get_user_model()") -> bool: + def user_is_sponsor(self, user: AbstractBaseUser) -> bool: """Returns whether the given user is a sponsor of the activity. Args: user: The user to check for sponsorship of this activity. @@ -870,7 +873,7 @@ def user_is_sponsor(self, user: "get_user_model()") -> bool: """ return self.get_true_sponsors().filter(user=user).exists() - def get_true_rooms(self) -> Union[QuerySet, Collection[EighthRoom]]: # pylint: disable=unsubscriptable-object + def get_true_rooms(self) -> QuerySet[EighthRoom]: # pylint: disable=unsubscriptable-object """Retrieves the rooms for the scheduled activity, taking into account activity defaults and overrides. Returns: @@ -972,7 +975,7 @@ def is_overbooked(self) -> bool: capacity = self.get_true_capacity() return capacity != -1 and self.eighthsignup_set.count() > capacity - def is_too_early_to_signup(self, now: Optional[datetime.datetime] = None) -> (bool, datetime): + def is_too_early_to_signup(self, now: datetime.datetime | None = None) -> (bool, datetime): """Returns whether it is too early to sign up for the activity if it is a presign. This contains the 2 day pre-signup logic. @@ -1000,7 +1003,7 @@ def has_open_passes(self) -> bool: """ return self.eighthsignup_set.filter(after_deadline=True, pass_accepted=False).exists() - def _get_viewable_members(self, user: "get_user_model()") -> Union[QuerySet, Collection["get_user_model()"]]: # pylint: disable=unsubscriptable-object + def _get_viewable_members(self, user: AbstractBaseUser) -> QuerySet[AbstractBaseUser]: # pylint: disable=unsubscriptable-object """Get an unsorted QuerySet of the members that you have permission to view. Args: user: The user who is attempting to view the member list. @@ -1015,7 +1018,7 @@ def _get_viewable_members(self, user: "get_user_model()") -> Union[QuerySet, Col q |= Q(id=user.id) return self.members.filter(q) - def get_viewable_members(self, user: "get_user_model()" = None) -> Union[QuerySet, Collection["get_user_model()"]]: # pylint: disable=unsubscriptable-object + def get_viewable_members(self, user: AbstractBaseUser | None = None) -> QuerySet[AbstractBaseUser]: # pylint: disable=unsubscriptable-object """Returns a QuerySet of the members that you have permission to view, sorted alphabetically. Args: user: The user who is attempting to view the member list. @@ -1024,7 +1027,7 @@ def get_viewable_members(self, user: "get_user_model()" = None) -> Union[QuerySe """ return self._get_viewable_members(user).order_by("last_name", "first_name") - def get_viewable_members_serializer(self, request) -> Union[QuerySet, Collection["get_user_model()"]]: # pylint: disable=unsubscriptable-object + def get_viewable_members_serializer(self, request) -> QuerySet[AbstractBaseUser]: # pylint: disable=unsubscriptable-object """Given a request, returns an unsorted QuerySet of the members that the requesting user has permission to view. Args: @@ -1034,7 +1037,7 @@ def get_viewable_members_serializer(self, request) -> Union[QuerySet, Collection """ return self._get_viewable_members(request.user) - def get_hidden_members(self, user: "get_user_model()" = None) -> Union[QuerySet, Collection["get_user_model()"]]: # pylint: disable=unsubscriptable-object + def get_hidden_members(self, user: AbstractBaseUser | None = None) -> QuerySet[AbstractBaseUser]: # pylint: disable=unsubscriptable-object """Returns a QuerySet of the members that you do not have permission to view. Args: user: The user who is attempting to view the member list. @@ -1050,7 +1053,7 @@ def get_hidden_members(self, user: "get_user_model()" = None) -> Union[QuerySet, return hidden_members - def get_both_blocks_sibling(self) -> Optional["EighthScheduledActivity"]: + def get_both_blocks_sibling(self) -> EighthScheduledActivity | None: """If this is a both-blocks activity, get the other EighthScheduledActivity object that occurs on the other block. both_blocks means A and B block, NOT all of the blocks on that day. @@ -1074,7 +1077,7 @@ def get_both_blocks_sibling(self) -> Optional["EighthScheduledActivity"]: except EighthScheduledActivity.DoesNotExist: return None - def notify_waitlist(self, waitlists: Iterable["EighthWaitlist"]): + def notify_waitlist(self, waitlists: Iterable[EighthWaitlist]): """Notifies all users on the given EighthWaitlist objects that the activity they are on the waitlist for has an open spot. Args: waitlists: The EighthWaitlist objects whose users should be notified that the activity has an open slot. @@ -1091,8 +1094,8 @@ def notify_waitlist(self, waitlists: Iterable["EighthWaitlist"]): @transaction.atomic # This MUST be run in a transaction. Do NOT remove this decorator. def add_user( self, - user: "get_user_model()", - request: Optional[HttpRequest] = None, + user: AbstractBaseUser, + request: HttpRequest | None = None, force: bool = False, no_after_deadline: bool = False, add_to_waitlist: bool = False, @@ -1533,7 +1536,7 @@ def __str__(self): class EighthSignupManager(Manager): """Model manager for EighthSignup.""" - def create_signup(self, user: "get_user_model()", scheduled_activity: "EighthScheduledActivity", **kwargs) -> "EighthSignup": + def create_signup(self, user: AbstractBaseUser, scheduled_activity: EighthScheduledActivity, **kwargs) -> EighthSignup: """Creates an EighthSignup for the given user in the given activity after checking for duplicate signups. This raises an error if there are duplicate signups. Args: @@ -1566,7 +1569,7 @@ def create_signup(self, user: "get_user_model()", scheduled_activity: "EighthSch return signup - def get_absences(self) -> Union[QuerySet, Collection["EighthSignup"]]: # pylint: disable=unsubscriptable-object + def get_absences(self) -> QuerySet[EighthSignup]: # pylint: disable=unsubscriptable-object """Returns all EighthSignups for which the student was marked as absent. Returns: A QuerySet of all the EighthSignups for which the student was marked as absent. @@ -1659,7 +1662,7 @@ def has_conflict(self, nocache: bool = False) -> bool: return q.exists() - def remove_signup(self, user: "get_user_model()" = None, force: bool = False, dont_run_waitlist: bool = False) -> str: + def remove_signup(self, user: AbstractBaseUser = None, force: bool = False, dont_run_waitlist: bool = False) -> str: """Attempts to remove the EighthSignup if the user has permission to do so. Args: user: The user who is attempting to remove the EighthSignup. @@ -1738,7 +1741,7 @@ class Meta: class EighthWaitlistManager(Manager): """Model manager for EighthWaitlist.""" - def get_next_waitlist(self, activity: EighthScheduledActivity) -> Union[QuerySet, Collection["EighthWaitlist"]]: # pylint: disable=unsubscriptable-object + def get_next_waitlist(self, activity: EighthScheduledActivity) -> QuerySet[EighthWaitlist]: # pylint: disable=unsubscriptable-object """Returns a QuerySet of all the EighthWaitlist objects for the given activity, ordered by signup time. Args: @@ -1749,7 +1752,7 @@ def get_next_waitlist(self, activity: EighthScheduledActivity) -> Union[QuerySet """ return self.filter(scheduled_activity_id=activity.id).order_by("time") - def check_for_prescence(self, activity: EighthScheduledActivity, user: "get_user_model()") -> bool: + def check_for_prescence(self, activity: EighthScheduledActivity, user: AbstractBaseUser) -> bool: """Returns whether the given user is in a waitlist for the given activity. Args: activity: The activity for which the waitlist should be queried. diff --git a/intranet/apps/eighth/tasks.py b/intranet/apps/eighth/tasks.py index 20d4326e92c..51ba4db6ecb 100644 --- a/intranet/apps/eighth/tasks.py +++ b/intranet/apps/eighth/tasks.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import calendar import datetime from typing import Collection diff --git a/intranet/apps/eighth/views/admin/groups.py b/intranet/apps/eighth/views/admin/groups.py index 55f82c79e51..a9581448f88 100644 --- a/intranet/apps/eighth/views/admin/groups.py +++ b/intranet/apps/eighth/views/admin/groups.py @@ -1,7 +1,8 @@ +from __future__ import annotations + import csv import logging import re -from typing import List, Optional from cacheops import invalidate_model, invalidate_obj from django import http @@ -155,7 +156,7 @@ def get_file_string(fileobj): return filetext -def get_user_info(key: str, val) -> Optional[List[User]]: +def get_user_info(key: str, val) -> list[User] | None: if key in ["username", "id"]: try: u = get_user_model().objects.filter(**{key: val}) @@ -199,7 +200,7 @@ def handle_group_input(filetext: str): return find_users_input(lines) -def find_users_input(lines: List[str]): +def find_users_input(lines: list[str]): sure_users = [] unsure_users = [] for line in lines: @@ -486,7 +487,7 @@ def eighth_admin_signup_group_action(request, group_id, schact_id): ) -def eighth_admin_perform_group_signup(*, group_id: int, schact_id: int, request: Optional[http.HttpRequest], skip_users: set): +def eighth_admin_perform_group_signup(*, group_id: int, schact_id: int, request: http.HttpRequest | None, skip_users: set): """Performs sign up of all users in a specific group up for a specific scheduled activity. diff --git a/intranet/apps/eighth/views/admin/hybrid.py b/intranet/apps/eighth/views/admin/hybrid.py index 9045ec3a625..ed223fa0917 100644 --- a/intranet/apps/eighth/views/admin/hybrid.py +++ b/intranet/apps/eighth/views/admin/hybrid.py @@ -1,5 +1,6 @@ +from __future__ import annotations + import logging -from typing import Optional from django import http from django.contrib import messages @@ -212,7 +213,7 @@ def eighth_admin_signup_group_action_hybrid(request, group_id, schact_virtual_id ) -def eighth_admin_perform_group_signup(*, group_id: int, schact_virtual_id: int, schact_person_id: int, request: Optional[http.HttpRequest]): +def eighth_admin_perform_group_signup(*, group_id: int, schact_virtual_id: int, schact_person_id: int, request: http.HttpRequest | None): """Performs sign up of all users in a specific group up for a specific scheduled activity. diff --git a/intranet/apps/features/helpers.py b/intranet/apps/features/helpers.py index 92c588e4a31..8d7ef357e58 100644 --- a/intranet/apps/features/helpers.py +++ b/intranet/apps/features/helpers.py @@ -1,7 +1,7 @@ -from typing import Optional +from __future__ import annotations -def get_feature_context(request) -> Optional[str]: +def get_feature_context(request) -> str | None: """Given a Django request, returns the 'context' that should be used to select feature announcements to display (one of ``dashboard``, ``login``, ``eighth_signup``, or ``None``). diff --git a/intranet/apps/notifications/emails.py b/intranet/apps/notifications/emails.py index 662051382fa..32ea17a482a 100644 --- a/intranet/apps/notifications/emails.py +++ b/intranet/apps/notifications/emails.py @@ -1,5 +1,7 @@ +from __future__ import annotations + import logging -from typing import Collection, Mapping +from typing import Mapping, MutableSequence from django.conf import settings from django.core.mail import EmailMultiAlternatives @@ -13,11 +15,11 @@ def email_send( html_template: str, data: Mapping[str, object], subject: str, - emails: Collection[str], # pylint: disable=unsubscriptable-object - headers: Mapping[str, str] = None, # pylint: disable=unsubscriptable-object + emails: MutableSequence[str], + headers: Mapping[str, str] | None = None, bcc: bool = False, *, - custom_logger: logging.Logger = None, + custom_logger: logging.Logger | None = None, ) -> EmailMultiAlternatives: """Send an HTML/Plaintext email with the following fields. If we are not in production and settings.FORCE_EMAIL_SEND is not set, does not actually send the email diff --git a/intranet/apps/printing/views.py b/intranet/apps/printing/views.py index 6e9e0562cf3..b3407aab654 100644 --- a/intranet/apps/printing/views.py +++ b/intranet/apps/printing/views.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import logging import math import os @@ -5,7 +7,6 @@ import subprocess import tempfile from io import BytesIO -from typing import Dict, Optional import magic from django.conf import settings @@ -62,14 +63,15 @@ def set_user_ratelimit_status(username: str) -> None: cache.incr(cache_key) -def get_printers() -> Dict[str, str]: +def get_printers() -> dict[str, str] | list: """Returns a dictionary mapping name:description for available printers. This requires that a CUPS client be configured on the server. Otherwise, this returns an empty dictionary. Returns: - A dictionary mapping name:description for available printers. + A dictionary mapping name:description for available printers, or + an empty list if cups isn't installed or lpstat fails """ key = "printing:printers" @@ -116,7 +118,7 @@ def get_printers() -> Dict[str, str]: return printers -def convert_soffice(tmpfile_name: str) -> Optional[str]: +def convert_soffice(tmpfile_name: str) -> str | None: """Converts a doc or docx to a PDF with soffice. Args: @@ -147,7 +149,7 @@ def convert_soffice(tmpfile_name: str) -> Optional[str]: return None -def convert_pdf(tmpfile_name: str, cmdname: str = "ps2pdf") -> Optional[str]: +def convert_pdf(tmpfile_name: str, cmdname: str = "ps2pdf") -> str | None: new_name = f"{tmpfile_name}.pdf" try: output = subprocess.check_output([cmdname, tmpfile_name, new_name], stderr=subprocess.STDOUT, universal_newlines=True) @@ -209,7 +211,7 @@ def get_mimetype(tmpfile_name: str) -> str: return mimetype -def convert_file(tmpfile_name: str, orig_fname: str) -> Optional[str]: +def convert_file(tmpfile_name: str, orig_fname: str) -> str | None: detected = get_mimetype(tmpfile_name) add_breadcrumb(category="printing", message=f"Detected file type {detected}", level="debug") @@ -241,7 +243,7 @@ def convert_file(tmpfile_name: str, orig_fname: str) -> Optional[str]: raise InvalidInputPrintingError(f"Invalid file type {detected}") -def check_page_range(page_range: str, max_pages: int) -> Optional[int]: +def check_page_range(page_range: str, max_pages: int) -> int | None: """Returns the number of pages included in the range, or None if it is an invalid range. Args: diff --git a/intranet/apps/signage/views.py b/intranet/apps/signage/views.py index 077981b8273..07443b4fe81 100644 --- a/intranet/apps/signage/views.py +++ b/intranet/apps/signage/views.py @@ -1,6 +1,7 @@ +from __future__ import annotations + import datetime import logging -from typing import Optional from django import http from django.conf import settings @@ -20,7 +21,7 @@ logger = logging.getLogger(__name__) -def check_internal_ip(request) -> Optional[HttpResponse]: +def check_internal_ip(request) -> HttpResponse | None: """ A method to determine if a request is allowed to load a signage page. diff --git a/intranet/apps/templatetags/paginate.py b/intranet/apps/templatetags/paginate.py index 22eed86ca88..5529a2cf6ba 100644 --- a/intranet/apps/templatetags/paginate.py +++ b/intranet/apps/templatetags/paginate.py @@ -1,4 +1,4 @@ -from typing import List, Union +from __future__ import annotations from django import template @@ -13,8 +13,8 @@ def query_transform(request, **kwargs): return query.urlencode() -@register.filter # TODO: replace return type with list[int | None] -def page_list(paginator, current_page) -> List[Union[int, None]]: +@register.filter +def page_list(paginator, current_page) -> list[int | None]: """Pagination If there is a ``None`` in the output, it should be replaced diff --git a/intranet/apps/users/models.py b/intranet/apps/users/models.py index d592100c261..7ccccaa1665 100644 --- a/intranet/apps/users/models.py +++ b/intranet/apps/users/models.py @@ -1,8 +1,9 @@ # pylint: disable=too-many-lines; Allow more than 1000 lines +from __future__ import annotations + import logging from base64 import b64encode from datetime import timedelta -from typing import Collection, Dict, Optional, Union from dateutil.relativedelta import relativedelta from django.conf import settings @@ -41,14 +42,14 @@ class UserManager(DjangoUserManager): """ - def user_with_student_id(self, student_id: Union[int, str]) -> Optional["User"]: + def user_with_student_id(self, student_id: int | str) -> User | None: """Get a unique user object by FCPS student ID. (Ex. 1624472)""" results = User.objects.filter(student_id=str(student_id)) if len(results) == 1: return results.first() return None - def user_with_ion_id(self, student_id: Union[int, str]) -> Optional["User"]: + def user_with_ion_id(self, student_id: int | str) -> User | None: """Get a unique user object by Ion ID. (Ex. 489)""" if isinstance(student_id, str) and not is_entirely_digit(student_id): return None @@ -57,11 +58,11 @@ def user_with_ion_id(self, student_id: Union[int, str]) -> Optional["User"]: return results.first() return None - def users_in_year(self, year: int) -> Union[Collection["User"], QuerySet]: # pylint: disable=unsubscriptable-object + def users_in_year(self, year: int) -> QuerySet[User]: # pylint: disable=unsubscriptable-object """Get a list of users in a specific graduation year.""" return User.objects.filter(graduation_year=year) - def user_with_name(self, given_name: Optional[str] = None, last_name: Optional[str] = None) -> "User": # pylint: disable=unsubscriptable-object + def user_with_name(self, given_name: str | None = None, last_name: str | None = None) -> User: # pylint: disable=unsubscriptable-object """Get a unique user object by given name (first/nickname) and/or last name. Args: @@ -85,14 +86,14 @@ def user_with_name(self, given_name: Optional[str] = None, last_name: Optional[s except (User.DoesNotExist, User.MultipleObjectsReturned): return None - def get_students(self) -> Union[Collection["User"], QuerySet]: # pylint: disable=unsubscriptable-object + def get_students(self) -> QuerySet[User]: # pylint: disable=unsubscriptable-object """Get user objects that are students (quickly).""" users = User.objects.filter(user_type="student", graduation_year__gte=get_senior_graduation_year()) users = users.exclude(id__in=EXTRA) return users - def get_teachers(self) -> Union[Collection["User"], QuerySet]: # pylint: disable=unsubscriptable-object + def get_teachers(self) -> QuerySet[User]: # pylint: disable=unsubscriptable-object """Get user objects that are teachers (quickly).""" users = User.objects.filter(user_type="teacher") users = users.exclude(id__in=EXTRA) @@ -103,7 +104,7 @@ def get_teachers(self) -> Union[Collection["User"], QuerySet]: # pylint: disabl return users - def get_teachers_attendance_users(self) -> "QuerySet[User]": + def get_teachers_attendance_users(self) -> QuerySet[User]: """Like ``get_teachers()``, but includes attendance-only users as well as teachers. @@ -120,7 +121,7 @@ def get_teachers_attendance_users(self) -> "QuerySet[User]": return users - def get_teachers_sorted(self) -> Union[Collection["User"], QuerySet]: # pylint: disable=unsubscriptable-object + def get_teachers_sorted(self) -> QuerySet[User]: # pylint: disable=unsubscriptable-object """Returns a ``QuerySet`` of teachers sorted by last name, then first name. Returns: @@ -129,7 +130,7 @@ def get_teachers_sorted(self) -> Union[Collection["User"], QuerySet]: # pylint: """ return self.get_teachers().order_by("last_name", "first_name") - def get_teachers_attendance_users_sorted(self) -> "QuerySet[User]": + def get_teachers_attendance_users_sorted(self) -> QuerySet[User]: """Returns a ``QuerySet`` containing both teachers and attendance-only users sorted by last name, then first name. @@ -139,7 +140,7 @@ def get_teachers_attendance_users_sorted(self) -> "QuerySet[User]": """ return self.get_teachers_attendance_users().order_by("last_name", "first_name") - def get_approve_announcements_users(self) -> "QuerySet[User]": + def get_approve_announcements_users(self) -> QuerySet[User]: """Returns a ``QuerySet`` containing all users except simple users, tjstar presenters, alumni, service users and students. @@ -155,7 +156,7 @@ def get_approve_announcements_users(self) -> "QuerySet[User]": return users - def get_approve_announcements_users_sorted(self) -> "QuerySet[User]": + def get_approve_announcements_users_sorted(self) -> QuerySet[User]: """Returns a ``QuerySet`` containing all users except simple users, tjstar presenters, alumni, service users and students sorted by last name, then first name. @@ -170,8 +171,8 @@ def get_approve_announcements_users_sorted(self) -> "QuerySet[User]": def exclude_from_search( self, - existing_queryset: Optional[Union[Collection["User"], QuerySet]] = None, # pylint: disable=unsubscriptable-object - ) -> Union[Collection["User"], QuerySet]: # pylint: disable=unsubscriptable-object + existing_queryset: QuerySet[User] | None = None, # pylint: disable=unsubscriptable-object + ) -> QuerySet[User]: # pylint: disable=unsubscriptable-object if existing_queryset is None: existing_queryset = self @@ -249,7 +250,7 @@ class User(AbstractBaseUser, PermissionsMixin): objects = UserManager() @staticmethod - def get_signage_user() -> "User": + def get_signage_user() -> User: """Returns the user used to authenticate signage displays Returns: @@ -259,7 +260,7 @@ def get_signage_user() -> "User": return User(id=99999) @property - def address(self) -> Optional["Address"]: + def address(self) -> Address | None: """Returns the ``Address`` object representing this user's address, or ``None`` if it is not set or the current user does not have permission to access it. @@ -271,7 +272,7 @@ def address(self) -> Optional["Address"]: return self.properties.address @property - def schedule(self) -> Optional[Union[QuerySet, Collection["Section"]]]: # pylint: disable=unsubscriptable-object + def schedule(self) -> QuerySet[Section] | None: # pylint: disable=unsubscriptable-object """Returns a QuerySet of the ``Section`` objects representing the classes this student is in, or ``None`` if the current user does not have permission to list this student's classes. @@ -283,7 +284,7 @@ def schedule(self) -> Optional[Union[QuerySet, Collection["Section"]]]: # pylin """ return self.properties.schedule - def member_of(self, group: Union[Group, str]) -> bool: + def member_of(self, group: Group | str) -> bool: """Returns whether a user is a member of a certain group. Args: @@ -401,7 +402,7 @@ def get_short_name(self) -> str: return self.short_name @property - def primary_email_address(self) -> Optional[str]: + def primary_email_address(self) -> str | None: try: return self.primary_email.address if self.primary_email else None except Email.DoesNotExist: @@ -433,7 +434,7 @@ def tj_email(self) -> str: return f"{self.username}@{domain}" @property - def non_tj_email(self) -> Optional[str]: + def non_tj_email(self) -> str | None: """ Returns the user's first non-TJ email found, or None if none is found. @@ -475,7 +476,7 @@ def notification_email(self) -> str: return email.address if email and email.address else self.tj_email @property - def default_photo(self) -> Optional[bytes]: + def default_photo(self) -> bytes | None: """Returns default photo (in binary) that should be used Returns: @@ -500,7 +501,7 @@ def default_photo(self) -> Optional[bytes]: return None @property - def grade(self) -> "Grade": + def grade(self) -> Grade: """Returns the grade of a user. Returns: @@ -510,7 +511,7 @@ def grade(self) -> "Grade": return Grade(self.graduation_year) @property - def permissions(self) -> Dict[str, bool]: + def permissions(self) -> dict[str, bool]: """Dynamically generate dictionary of privacy options. Returns: @@ -752,7 +753,7 @@ def is_global_admin(self) -> bool: return self.member_of("admin_all") and self.is_staff and self.is_superuser - def can_manage_group(self, group: Union[Group, str]) -> bool: + def can_manage_group(self, group: Group | str) -> bool: """Checks whether this user has permission to edit/manage the given group (either a Group or a group name). @@ -1380,7 +1381,7 @@ def __getattr__(self, name): raise AttributeError(f"{type(self).__name__!r} object has no attribute {name!r}") @cached_property - def base64(self) -> Optional[bytes]: + def base64(self) -> bytes | None: """Returns base64 encoded binary data for a user's picture. Returns: @@ -1446,7 +1447,7 @@ def text(self) -> str: return self._name @staticmethod - def number_from_name(name: str) -> Optional[int]: + def number_from_name(name: str) -> int | None: if name in Grade.names: return Grade.names.index(name) + 9 return None diff --git a/intranet/settings/__init__.py b/intranet/settings/__init__.py index c073348baaa..f5aa91f3787 100644 --- a/intranet/settings/__init__.py +++ b/intranet/settings/__init__.py @@ -1,9 +1,11 @@ +from __future__ import annotations + import datetime import logging import os import re import sys -from typing import Any, Dict, List, Tuple # noqa +from typing import Any import celery.schedules import pytz @@ -88,11 +90,11 @@ # Dummy values for development and testing. # Overridden by the import from secret.py below. -SECRET_DATABASE_URL = None # type: str -MAINTENANCE_MODE = None # type: bool -TJSTAR_MAP = None # type: bool -TWITTER_KEYS = None # type: Dict[str,str] -SENTRY_PUBLIC_DSN = None # type: str +SECRET_DATABASE_URL: str | None = None +MAINTENANCE_MODE: bool | None = None +TJSTAR_MAP: bool | None = None +TWITTER_KEYS: dict[str, str] | None = None +SENTRY_PUBLIC_DSN: str | None = None USE_SASL = True NO_CACHE = False PARKING_ENABLED = True @@ -124,7 +126,7 @@ ALLOWED_METRIC_SCRAPE_IPS = [] -EMERGENCY_MESSAGE = None # type: str +EMERGENCY_MESSAGE: str | None = None # Hosts/domain names that are valid for this site; required if DEBUG is False # See https://docs.djangoproject.com/en/dev/ref/settings/#allowed-hosts @@ -202,7 +204,7 @@ EMAIL_FROM = "ion-noreply@tjhsst.edu" # Use PostgreSQL database -DATABASES = {"default": {"ENGINE": "django_prometheus.db.backends.postgresql", "CONN_MAX_AGE": 30}} # type: Dict[str,Dict[str,Any]] +DATABASES: dict[str, dict[str, Any]] = {"default": {"ENGINE": "django_prometheus.db.backends.postgresql", "CONN_MAX_AGE": 30}} # Address to send feedback messages to FEEDBACK_EMAIL = "intranet@tjhsst.edu" @@ -244,7 +246,7 @@ ] # Local time zone for this installation. Choices can be found here: -# http://en.wikipedia.org/wiki/List_of_tz_zones_by_name +# https://en.wikipedia.org/wiki/List_of_tz_zones_by_name # although not all choices may be available on all operating systems. # In a Windows environment this must be set to your system time zone. TIME_ZONE = "America/New_York" @@ -313,11 +315,11 @@ STATICFILES_STORAGE = "pipeline.storage.PipelineStorage" -PIPELINE = { +PIPELINE: dict[str, Any] = { "CSS_COMPRESSOR": None, "COMPILERS": ["pipeline.compilers.sass.SASSCompiler"], "STYLESHEETS": {}, -} # type: Dict[str,Any] +} LIST_OF_INDEPENDENT_CSS = [ "about", @@ -419,7 +421,7 @@ # Use the custom User model defined in apps/users/models.py AUTH_USER_MODEL = "users.User" -TEMPLATES = [ +TEMPLATES: list[dict[str, Any]] = [ { "BACKEND": "django.template.backends.django.DjangoTemplates", "DIRS": (os.path.join(PROJECT_ROOT, "templates"),), @@ -451,7 +453,7 @@ "libraries": {"staticfiles": "django.templatetags.static"}, }, } -] # type: List[Dict[str,Any]] +] if PRODUCTION: TEMPLATES[0]["OPTIONS"]["loaders"] = [ @@ -561,14 +563,14 @@ def get_month_seconds(): SESSION_COOKIE_AGE = int(datetime.timedelta(hours=2).total_seconds()) SESSION_SAVE_EVERY_REQUEST = True -CACHES = { +CACHES: dict[str, dict[str, Any]] = { "default": { "OPTIONS": { # Avoid conflict between production and testing redis db "DB": (1 if PRODUCTION else 2) } } -} # type: Dict[str,Dict[str,Any]] +} if TESTING or os.getenv("DUMMY_CACHE", "NO") == "YES" or NO_CACHE: CACHES["default"] = {"BACKEND": "intranet.utils.cache.DummyCache"} diff --git a/intranet/utils/helpers.py b/intranet/utils/helpers.py index 9935a8308f4..16e232d649e 100644 --- a/intranet/utils/helpers.py +++ b/intranet/utils/helpers.py @@ -1,9 +1,11 @@ +from __future__ import annotations + import datetime import ipaddress import logging import string import subprocess -from typing import Collection, Dict, Set # noqa +from typing import Collection from urllib import parse from django.conf import settings @@ -98,7 +100,7 @@ def __mod__(self, other): class MigrationMock: - seen = set() # type: Set[str] + seen: set[str] = set() def __contains__(self, mod): return True @@ -245,7 +247,7 @@ def get_theme_name() -> str: return None -def get_theme() -> Dict[str, Dict[str, str]]: +def get_theme() -> dict[str, dict[str, str]]: """Return JS and CSS for the currently active special event theme.""" return GLOBAL_THEMES.get(get_theme_name(), {}) diff --git a/intranet/utils/html.py b/intranet/utils/html.py index 88781af0c33..45ab80dd759 100644 --- a/intranet/utils/html.py +++ b/intranet/utils/html.py @@ -1,5 +1,7 @@ +from __future__ import annotations + import urllib.parse -from typing import Mapping, Optional, Tuple, Union +from typing import MutableMapping import bleach from bleach.css_sanitizer import CSSSanitizer @@ -35,9 +37,9 @@ def safe_html(txt): return bleach.linkify(bleach.clean(txt, tags=tags, attributes=att, css_sanitizer=css_sanitizer)) -def link_removal_callback( # pylint: disable=unused-argument - attrs: Mapping[Union[str, Tuple[Optional[str]]], str], new: bool = False -) -> Optional[Mapping[Union[str, Tuple[Optional[str]]], str]]: +def link_removal_callback( + attrs: MutableMapping[str | tuple[str | None], str], new: bool = False # pylint: disable=unused-argument +) -> MutableMapping[str | tuple[str | None], str] | None: """Internal callback for ``nullify_links()``.""" for key in tuple(attrs.keys()): if isinstance(key, tuple) and "href" in key: @@ -61,9 +63,9 @@ def nullify_links(text: str) -> str: def safe_fcps_emerg_html(text: str, base_url: str) -> str: - def translate_link_attr( # pylint: disable=unused-argument - attrs: Mapping[Union[str, Tuple[Optional[str]]], str], new: bool = False - ) -> Optional[Mapping[Union[str, Tuple[Optional[str]]], str]]: + def translate_link_attr( + attrs: MutableMapping[str | tuple[str | None], str], new: bool = False # pylint: disable=unused-argument + ) -> MutableMapping[str | tuple[str | None], str] | None: for key in tuple(attrs.keys()): if isinstance(key, tuple) and "href" in key: # Translate links that don't specify a protocol/host diff --git a/intranet/utils/locking.py b/intranet/utils/locking.py index d6bec42b4fa..f72bb74a844 100644 --- a/intranet/utils/locking.py +++ b/intranet/utils/locking.py @@ -1,9 +1,11 @@ -from typing import Dict, Iterable, List, Union +from __future__ import annotations + +from typing import Iterable from django.db.models import Manager, Model, QuerySet -def lock_on(items: Iterable[Union[Model, Manager, QuerySet]]) -> None: +def lock_on(items: Iterable[Model | Manager | QuerySet]) -> None: """Given an iterable of ``Model`` instances, ``Manager``s, and/or ``QuerySet``s, locks the corresponding database rows. @@ -26,15 +28,15 @@ def lock_on(items: Iterable[Union[Model, Manager, QuerySet]]) -> None: the database rows to lock. """ - querysets_by_model: Dict[str, List[Union[Manager, QuerySet]]] = {} - objects_by_model: Dict[str, List[Model]] = {} + querysets_by_model: dict[str, list[Manager | QuerySet]] = {} + objects_by_model: dict[str, list[Model]] = {} # First, we go through and categorize everything. Put instances in objects_by_model and # Managers/QuerySets in querysets_by_model. # Both are categorized by the dotted path to their class. for item in items: model_class = item.model if isinstance(item, (Manager, QuerySet)) else item.__class__ - model_fullname = model_class.__module__ + "." + model_class.__qualname__ + model_fullname = f"{model_class.__module__}.{model_class.__qualname__}" if isinstance(item, (Manager, QuerySet)): querysets_by_model.setdefault(model_fullname, []) diff --git a/pyproject.toml b/pyproject.toml index b71f675f3df..62ddc793212 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -108,8 +108,6 @@ ignore = [ "RUF005", # mutable class attrs annotated as typing.ClassVar "RUF012", - # implicit Optional - "RUF013", ] [tool.ruff.lint.per-file-ignores]