diff --git a/bookwyrm/models/book.py b/bookwyrm/models/book.py index 368276523..b883943c8 100644 --- a/bookwyrm/models/book.py +++ b/bookwyrm/models/book.py @@ -1,8 +1,9 @@ """ database schema for books and shelves """ +from __future__ import annotations from itertools import chain import re -from typing import Any, Dict, Optional, Iterable +from typing import Any, Dict, Optional, Iterable, TYPE_CHECKING from typing_extensions import Self from django.contrib.postgres.search import SearchVectorField @@ -33,6 +34,9 @@ from .activitypub_mixin import OrderedCollectionPageMixin, ObjectMixin from .base_model import BookWyrmModel from . import fields +if TYPE_CHECKING: + from bookwyrm.models import Author + class BookDataModel(ObjectMixin, BookWyrmModel): """fields shared between editable book data (books, works, authors)""" @@ -415,7 +419,7 @@ class Work(OrderedCollectionPageMixin, Book): """in case the default edition is not set""" return self.editions.order_by("-edition_rank").first() - def author_edition(self, author): + def author_edition(self, author: Author) -> Any: """in case the default edition doesn't have the required author""" return self.editions.filter(authors=author).order_by("-edition_rank").first() diff --git a/bookwyrm/templatetags/book_display_tags.py b/bookwyrm/templatetags/book_display_tags.py index 0a0f228d8..9e0e97063 100644 --- a/bookwyrm/templatetags/book_display_tags.py +++ b/bookwyrm/templatetags/book_display_tags.py @@ -1,5 +1,7 @@ """ template filters """ +from typing import Any from django import template +from django.db.models import QuerySet from bookwyrm import models @@ -7,13 +9,13 @@ register = template.Library() @register.filter(name="review_count") -def get_review_count(book): +def get_review_count(book: models.Edition) -> int: """how many reviews?""" return models.Review.objects.filter(deleted=False, book=book).count() @register.filter(name="book_description") -def get_book_description(book): +def get_book_description(book: models.Edition) -> Any: """use the work's text if the book doesn't have it""" if book.description: return book.description @@ -24,12 +26,12 @@ def get_book_description(book): @register.simple_tag(takes_context=False) -def get_book_file_links(book): +def get_book_file_links(book: models.Edition) -> QuerySet[models.FileLink]: """links for a book""" return book.file_links.filter(domain__status="approved") @register.filter(name="author_edition") -def get_author_edition(book, author): +def get_author_edition(book: models.Work, author: models.Author) -> Any: """default edition for a book on the author page""" return book.author_edition(author) diff --git a/bookwyrm/templatetags/celery_tags.py b/bookwyrm/templatetags/celery_tags.py index 6168d048e..9bf99ebb9 100644 --- a/bookwyrm/templatetags/celery_tags.py +++ b/bookwyrm/templatetags/celery_tags.py @@ -7,18 +7,18 @@ register = template.Library() @register.filter(name="uptime") -def uptime(seconds): +def uptime(seconds: float) -> str: """Seconds uptime to a readable format""" return str(datetime.timedelta(seconds=seconds)) @register.filter(name="runtime") -def runtime(timestamp): +def runtime(timestamp: float) -> datetime.timedelta: """How long has it been?""" return datetime.datetime.now() - datetime.datetime.fromtimestamp(timestamp) @register.filter(name="shortname") -def shortname(name): +def shortname(name: str) -> str: """removes bookwyrm.celery...""" return ".".join(name.split(".")[-2:]) diff --git a/bookwyrm/templatetags/date_ext.py b/bookwyrm/templatetags/date_ext.py index efe55f2d9..de8131b4d 100644 --- a/bookwyrm/templatetags/date_ext.py +++ b/bookwyrm/templatetags/date_ext.py @@ -1,4 +1,6 @@ """ additional formatting of dates """ +from typing import Any + from django import template from django.template import defaultfilters from django.contrib.humanize.templatetags.humanize import naturalday @@ -9,7 +11,7 @@ register = template.Library() @register.filter(expects_localtime=True) -def naturalday_partial(date, arg=None): +def naturalday_partial(date: Any, arg: Any = None) -> str | None: """chooses appropriate precision if date is a PartialDate object If arg is a Django-defined format such as "DATE_FORMAT", it will be adjusted diff --git a/bookwyrm/templatetags/feed_page_tags.py b/bookwyrm/templatetags/feed_page_tags.py index 3d346b9a2..fdba8b9ee 100644 --- a/bookwyrm/templatetags/feed_page_tags.py +++ b/bookwyrm/templatetags/feed_page_tags.py @@ -1,13 +1,15 @@ """ tags used on the feed pages """ +from typing import Any from django import template from bookwyrm.views.feed import get_suggested_books +from bookwyrm import models register = template.Library() @register.filter(name="load_subclass") -def load_subclass(status): +def load_subclass(status: models.Status) -> models.Status: """sometimes you didn't select_subclass""" if hasattr(status, "quotation"): return status.quotation @@ -21,7 +23,7 @@ def load_subclass(status): @register.simple_tag(takes_context=True) -def suggested_books(context): +def suggested_books(context: dict[str, Any]) -> Any: """get books for suggested books panel""" # this happens here instead of in the view so that the template snippet can # be cached in the template diff --git a/bookwyrm/templatetags/group_tags.py b/bookwyrm/templatetags/group_tags.py index fde7997e8..99f8e4eb8 100644 --- a/bookwyrm/templatetags/group_tags.py +++ b/bookwyrm/templatetags/group_tags.py @@ -7,21 +7,21 @@ register = template.Library() @register.filter(name="has_groups") -def has_groups(user): +def has_groups(user: models.User) -> bool: """whether or not the user has a pending invitation to join this group""" return models.GroupMember.objects.filter(user=user).exists() @register.filter(name="is_member") -def is_member(group, user): +def is_member(group: models.Group, user: models.User) -> bool: """whether or not the user is a member of this group""" return models.GroupMember.objects.filter(group=group, user=user).exists() @register.filter(name="is_invited") -def is_invited(group, user): +def is_invited(group: models.Group, user: models.User) -> bool: """whether or not the user has a pending invitation to join this group""" return models.GroupMemberInvitation.objects.filter(group=group, user=user).exists() diff --git a/bookwyrm/templatetags/interaction.py b/bookwyrm/templatetags/interaction.py index 9c73aa1af..37ac0ef62 100644 --- a/bookwyrm/templatetags/interaction.py +++ b/bookwyrm/templatetags/interaction.py @@ -1,4 +1,5 @@ """ template filters for status interaction buttons """ +from typing import Any from django import template from bookwyrm import models @@ -9,7 +10,7 @@ register = template.Library() @register.filter(name="liked") -def get_user_liked(user, status): +def get_user_liked(user: models.User, status: models.Status) -> Any: """did the given user fav a status?""" return get_or_set( f"fav-{user.id}-{status.id}", @@ -21,7 +22,7 @@ def get_user_liked(user, status): @register.filter(name="boosted") -def get_user_boosted(user, status): +def get_user_boosted(user: models.User, status: models.Status) -> Any: """did the given user fav a status?""" return get_or_set( f"boost-{user.id}-{status.id}", @@ -32,13 +33,13 @@ def get_user_boosted(user, status): @register.filter(name="saved") -def get_user_saved_lists(user, book_list): +def get_user_saved_lists(user: models.User, book_list: models.List) -> bool: """did the user save a list""" return user.saved_lists.filter(id=book_list.id).exists() @register.simple_tag(takes_context=True) -def get_relationship(context, user_object): +def get_relationship(context: dict[str, Any], user_object: models.User) -> Any: """caches the relationship between the logged in user and another user""" user = context["request"].user return get_or_set( @@ -50,7 +51,9 @@ def get_relationship(context, user_object): ) -def get_relationship_name(user, user_object): +def get_relationship_name( + user: models.User, user_object: models.User +) -> dict[str, bool]: """returns the relationship type""" types = { "is_following": False, diff --git a/bookwyrm/templatetags/landing_page_tags.py b/bookwyrm/templatetags/landing_page_tags.py index bc7594fc4..c61a9f56a 100644 --- a/bookwyrm/templatetags/landing_page_tags.py +++ b/bookwyrm/templatetags/landing_page_tags.py @@ -1,6 +1,7 @@ """ template filters """ +from typing import Optional from django import template -from django.db.models import Avg, StdDev, Count, F, Q +from django.db.models import Avg, StdDev, Count, F, Q, QuerySet from bookwyrm import models @@ -8,7 +9,7 @@ register = template.Library() @register.simple_tag(takes_context=False) -def get_book_superlatives(): +def get_book_superlatives() -> dict[str, Optional[models.Work]]: """get book stats for the about page""" total_ratings = models.Review.objects.filter(local=True, deleted=False).count() data = {} @@ -67,7 +68,7 @@ def get_book_superlatives(): @register.simple_tag(takes_context=False) -def get_landing_books(): +def get_landing_books() -> list[QuerySet[models.Edition]]: """list of books for the landing page""" return list( set( diff --git a/bookwyrm/templatetags/layout.py b/bookwyrm/templatetags/layout.py index f42f3bda1..9de5ec390 100644 --- a/bookwyrm/templatetags/layout.py +++ b/bookwyrm/templatetags/layout.py @@ -5,7 +5,7 @@ register = template.Library() @register.simple_tag(takes_context=False) -def get_lang(): +def get_lang() -> str: """get current language, strip to the first two letters""" language = utils.translation.get_language() return language[0 : language.find("-")] diff --git a/bookwyrm/templatetags/markdown.py b/bookwyrm/templatetags/markdown.py index 370d60a1a..c54f73813 100644 --- a/bookwyrm/templatetags/markdown.py +++ b/bookwyrm/templatetags/markdown.py @@ -1,4 +1,5 @@ """ template filters """ +from typing import Any from django import template from bookwyrm.views.status import to_markdown @@ -7,7 +8,7 @@ register = template.Library() @register.filter(name="to_markdown") -def get_markdown(content): +def get_markdown(content: str) -> Any: """convert markdown to html""" if content: return to_markdown(content) diff --git a/bookwyrm/templatetags/notification_page_tags.py b/bookwyrm/templatetags/notification_page_tags.py index 7a365e689..dc5a5b636 100644 --- a/bookwyrm/templatetags/notification_page_tags.py +++ b/bookwyrm/templatetags/notification_page_tags.py @@ -1,5 +1,7 @@ """ tags used on the feed pages """ +from typing import Optional from django import template +from bookwyrm import models from bookwyrm.templatetags.feed_page_tags import load_subclass @@ -7,7 +9,7 @@ register = template.Library() @register.simple_tag(takes_context=False) -def related_status(notification): +def related_status(notification: models.Notification) -> Optional[models.Status]: """for notifications""" if not notification.related_status: return None @@ -15,6 +17,6 @@ def related_status(notification): @register.simple_tag(takes_context=False) -def get_related_users(notification): +def get_related_users(notification: models.Notification) -> list[models.User]: """Who actually was it who liked your post""" return list(reversed(list(notification.related_users.distinct())))[:10] diff --git a/bookwyrm/templatetags/rating_tags.py b/bookwyrm/templatetags/rating_tags.py index 367463a8f..d14129633 100644 --- a/bookwyrm/templatetags/rating_tags.py +++ b/bookwyrm/templatetags/rating_tags.py @@ -1,4 +1,5 @@ """ template filters """ +from typing import Any from django import template from django.db.models import Avg @@ -10,7 +11,7 @@ register = template.Library() @register.filter(name="rating") -def get_rating(book, user): +def get_rating(book: models.Edition, user: models.User) -> Any: """get the overall rating of a book""" # this shouldn't happen, but it CAN if not book.parent_work: @@ -29,7 +30,7 @@ def get_rating(book, user): @register.filter(name="user_rating") -def get_user_rating(book, user): +def get_user_rating(book: models.Edition, user: models.User) -> Any: """get a user's rating of a book""" rating = ( models.Review.objects.filter( diff --git a/bookwyrm/templatetags/shelf_tags.py b/bookwyrm/templatetags/shelf_tags.py index 36065d575..87e7d4e39 100644 --- a/bookwyrm/templatetags/shelf_tags.py +++ b/bookwyrm/templatetags/shelf_tags.py @@ -1,6 +1,8 @@ """ Filters and tags related to shelving books """ +from typing import Any from django import template from django.utils.translation import gettext_lazy as _ +from django_stubs_ext import StrPromise from bookwyrm import models from bookwyrm.utils import cache @@ -19,7 +21,7 @@ SHELF_NAMES = { @register.filter(name="is_book_on_shelf") -def get_is_book_on_shelf(book, shelf): +def get_is_book_on_shelf(book: models.Edition, shelf: models.Shelf) -> Any: """is a book on a shelf""" return cache.get_or_set( f"book-on-shelf-{book.id}-{shelf.id}", @@ -31,7 +33,7 @@ def get_is_book_on_shelf(book, shelf): @register.filter(name="next_shelf") -def get_next_shelf(current_shelf): +def get_next_shelf(current_shelf: str) -> str: """shelf you'd use to update reading progress""" if current_shelf == "to-read": return "reading" @@ -45,7 +47,7 @@ def get_next_shelf(current_shelf): @register.filter(name="translate_shelf_name") -def get_translated_shelf_name(shelf): +def get_translated_shelf_name(shelf: models.Shelf | dict[str, str]) -> str | StrPromise: """produce translated shelf identifiername""" if not shelf: return "" @@ -59,7 +61,7 @@ def get_translated_shelf_name(shelf): @register.simple_tag(takes_context=True) -def active_shelf(context, book): +def active_shelf(context: dict[str, Any], book: models.Edition) -> Any: """check what shelf a user has a book on, if any""" user = context["request"].user return cache.get_or_set( @@ -78,7 +80,7 @@ def active_shelf(context, book): @register.simple_tag(takes_context=False) -def latest_read_through(book, user): +def latest_read_through(book: models.Edition, user: models.User) -> Any: """the most recent read activity""" return cache.get_or_set( f"latest_read_through-{user.id}-{book.id}", diff --git a/bookwyrm/templatetags/stars.py b/bookwyrm/templatetags/stars.py index d08dd8ef0..b3bbdf194 100644 --- a/bookwyrm/templatetags/stars.py +++ b/bookwyrm/templatetags/stars.py @@ -6,6 +6,6 @@ register = template.Library() @register.filter(name="half_star") -def get_half_star(value): +def get_half_star(value: str) -> str: """one of those things that's weirdly hard with templates""" return f"{value}.5" diff --git a/bookwyrm/templatetags/status_display.py b/bookwyrm/templatetags/status_display.py index c92b1877a..cf526c099 100644 --- a/bookwyrm/templatetags/status_display.py +++ b/bookwyrm/templatetags/status_display.py @@ -1,4 +1,5 @@ """ template filters """ +from typing import Any, Optional from dateutil.relativedelta import relativedelta from django import template from django.contrib.humanize.templatetags.humanize import naturaltime, naturalday @@ -12,7 +13,7 @@ register = template.Library() @register.filter(name="mentions") -def get_mentions(status, user): +def get_mentions(status: models.Status, user: models.User) -> str: """people to @ in a reply: the parent and all mentions""" mentions = set([status.user] + list(status.mention_users.all())) return ( @@ -21,7 +22,7 @@ def get_mentions(status, user): @register.filter(name="replies") -def get_replies(status): +def get_replies(status: models.Status) -> Any: """get all direct replies to a status""" # TODO: this limit could cause problems return models.Status.objects.filter( @@ -31,7 +32,7 @@ def get_replies(status): @register.filter(name="parent") -def get_parent(status): +def get_parent(status: models.Status) -> Any: """get the reply parent for a status""" return ( models.Status.objects.filter(id=status.reply_parent_id) @@ -41,7 +42,7 @@ def get_parent(status): @register.filter(name="boosted_status") -def get_boosted(boost): +def get_boosted(boost: models.Boost) -> Any: """load a boosted status. have to do this or it won't get foreign keys""" return ( models.Status.objects.select_subclasses() @@ -52,7 +53,7 @@ def get_boosted(boost): @register.filter(name="published_date") -def get_published_date(date): +def get_published_date(date: str) -> Any: """less verbose combo of humanize filters""" if not date: return "" @@ -66,7 +67,7 @@ def get_published_date(date): @register.simple_tag() -def get_header_template(status): +def get_header_template(status: models.Status) -> Any: """get the path for the status template""" if isinstance(status, models.Boost): status = status.boosted_status @@ -82,6 +83,6 @@ def get_header_template(status): @register.simple_tag(takes_context=False) -def load_book(status): +def load_book(status: models.Status) -> Optional[models.Book]: """how many users that you follow, follow them""" return status.book if hasattr(status, "book") else status.mention_books.first() diff --git a/bookwyrm/templatetags/user_page_tags.py b/bookwyrm/templatetags/user_page_tags.py index b3a82597e..1de4432f7 100644 --- a/bookwyrm/templatetags/user_page_tags.py +++ b/bookwyrm/templatetags/user_page_tags.py @@ -1,12 +1,14 @@ """ template filters """ +from typing import Any, Optional from django import template +from bookwyrm import models register = template.Library() @register.simple_tag(takes_context=True) -def mutuals_count(context, user): +def mutuals_count(context: dict[str, Any], user: models.User) -> Optional[int]: """how many users that you follow, follow them""" viewer = context["request"].user if not viewer.is_authenticated: diff --git a/bookwyrm/templatetags/utilities.py b/bookwyrm/templatetags/utilities.py index ab597a22a..0aabb94f7 100644 --- a/bookwyrm/templatetags/utilities.py +++ b/bookwyrm/templatetags/utilities.py @@ -1,46 +1,50 @@ """ template filters for really common utilities """ +from typing import Any, Optional + import os import re from uuid import uuid4 from urllib.parse import urlparse from django import template +from django.contrib.auth.models import Group from django.utils.safestring import mark_safe from django.utils.translation import gettext_lazy as _ from django.templatetags.static import static +from django_stubs_ext import StrPromise -from bookwyrm.models import User +from bookwyrm.models import Author, Edition, User from bookwyrm.settings import INSTANCE_ACTOR_USERNAME register = template.Library() @register.filter(name="uuid") -def get_uuid(identifier): +def get_uuid(identifier: str) -> str: """for avoiding clashing ids when there are many forms""" return f"{identifier}{uuid4()}" @register.simple_tag(takes_context=False) -def join(*args): +def join(*args: tuple[Any]) -> str: """concatenate an arbitrary set of values""" return "_".join(str(a) for a in args) @register.filter(name="username") -def get_user_identifier(user): +def get_user_identifier(user: User) -> str: """use localname for local users, username for remote""" - return user.localname if user.localname else user.username + return user.localname if user.localname else user.username or "" @register.filter(name="user_from_remote_id") -def get_user_identifier_from_remote_id(remote_id): +def get_user_identifier_from_remote_id(remote_id: str) -> Optional[User]: """get the local user id from their remote id""" user = User.objects.get(remote_id=remote_id) return user if user else None @register.filter(name="book_title") -def get_title(book, too_short=5): +def get_title(book: Edition, too_short: int = 5) -> Any: """display the subtitle if the title is short""" if not book: return "" @@ -54,7 +58,7 @@ def get_title(book, too_short=5): @register.simple_tag(takes_context=False) -def comparison_bool(str1, str2, reverse=False): +def comparison_bool(str1: str, str2: str, reverse: bool = False) -> bool: """idk why I need to write a tag for this, it returns a bool""" if reverse: return str1 != str2 @@ -62,7 +66,7 @@ def comparison_bool(str1, str2, reverse=False): @register.filter(is_safe=True) -def truncatepath(value, arg): +def truncatepath(value: Any, arg: Any) -> Any: """Truncate a path by removing all directories except the first and truncating""" path = os.path.normpath(value.name) path_list = path.split(os.sep) @@ -74,7 +78,9 @@ def truncatepath(value, arg): @register.simple_tag(takes_context=False) -def get_book_cover_thumbnail(book, size="medium", ext="jpg"): +def get_book_cover_thumbnail( + book: Edition, size: str = "medium", ext: str = "jpg" +) -> Any: """Returns a book thumbnail at the specified size and extension, with fallback if needed""" if size == "": @@ -87,7 +93,7 @@ def get_book_cover_thumbnail(book, size="medium", ext="jpg"): @register.filter(name="get_isni_bio") -def get_isni_bio(existing, author): +def get_isni_bio(existing: int, author: Author) -> str: """Returns the isni bio string if an existing author has an isni listed""" auth_isni = re.sub(r"\D", "", str(author.isni)) if len(existing) == 0: @@ -101,7 +107,7 @@ def get_isni_bio(existing, author): # pylint: disable=unused-argument @register.filter(name="get_isni", needs_autoescape=True) -def get_isni(existing, author, autoescape=True): +def get_isni(existing: str, author: Author, autoescape: bool = True) -> str: """Returns the isni ID if an existing author has an ISNI listing""" auth_isni = re.sub(r"\D", "", str(author.isni)) if len(existing) == 0: @@ -116,7 +122,7 @@ def get_isni(existing, author, autoescape=True): @register.simple_tag(takes_context=False) -def id_to_username(user_id): +def id_to_username(user_id: str) -> str | StrPromise: """given an arbitrary remote id, return the username""" if user_id: url = urlparse(user_id) @@ -130,7 +136,7 @@ def id_to_username(user_id): @register.filter(name="get_file_size") -def get_file_size(nbytes): +def get_file_size(nbytes: int) -> str: """display the size of a file in human readable terms""" try: @@ -148,13 +154,13 @@ def get_file_size(nbytes): @register.filter(name="get_user_permission") -def get_user_permission(user): +def get_user_permission(user: User) -> Group | str: """given a user, return their permission level""" return user.groups.first() or "User" @register.filter(name="is_instance_admin") -def is_instance_admin(localname): +def is_instance_admin(localname: str) -> bool: """Returns a boolean indicating whether the user is the instance admin account""" return localname == INSTANCE_ACTOR_USERNAME diff --git a/mypy.ini b/mypy.ini index 95f97ee93..567ca7d3f 100644 --- a/mypy.ini +++ b/mypy.ini @@ -10,23 +10,19 @@ django_settings_module = "bookwyrm.settings" ignore_errors = True implicit_reexport = True -[mypy-bookwyrm.connectors.*] -ignore_errors = False - [mypy-bookwyrm.models.author] ignore_errors = False allow_untyped_calls = True disable_error_code = import-untyped, assignment +[mypy-bookwyrm.connectors.*] [mypy-bookwyrm.utils.*] -ignore_errors = False - [mypy-bookwyrm.importers.*] -ignore_errors = False - [mypy-bookwyrm.isbn.*] -ignore_errors = False - [mypy-celerywyrm.*] ignore_errors = False +[mypy-bookwyrm.templatetags.*] +ignore_errors = False +allow_untyped_calls = True +disable_error_code = attr-defined, arg-type, misc