Skip to content

Commit

Permalink
feat: improve mypy hints
Browse files Browse the repository at this point in the history
  • Loading branch information
nijel committed Oct 22, 2024
1 parent f5f9be0 commit 2442a7b
Show file tree
Hide file tree
Showing 14 changed files with 137 additions and 43 deletions.
20 changes: 17 additions & 3 deletions weblate_web/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@
# along with this program. If not, see <https://www.gnu.org/licenses/>.
#

from __future__ import annotations

from typing import TYPE_CHECKING, Any

from django.contrib import admin

from weblate_web.models import (
Expand All @@ -30,6 +34,10 @@
Subscription,
)

if TYPE_CHECKING:
from django.forms import ModelForm
from django.http import HttpRequest


def format_user(obj):
return f"{obj.username}: {obj.first_name} {obj.last_name} <{obj.email}>"
Expand Down Expand Up @@ -85,9 +93,15 @@ class ServiceAdmin(admin.ModelAdmin):
autocomplete_fields = ("users", "customer")
inlines = (ProjectAdmin,)

def get_form(self, request, obj=None, **kwargs):
form = super().get_form(request, obj, **kwargs)
form.base_fields["users"].label_from_instance = format_user
def get_form(
self,
request: HttpRequest,
obj: Any | None = None,
change: bool = False,
**kwargs: Any,
) -> type[ModelForm[Any]]:
form = super().get_form(request=request, obj=obj, change=change, **kwargs)
form.base_fields["users"].label_from_instance = format_user # type: ignore[attr-defined]
return form


Expand Down
4 changes: 3 additions & 1 deletion weblate_web/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
# along with this program. If not, see <https://www.gnu.org/licenses/>.
#

from __future__ import annotations

from django import forms
from django.conf import settings
from django.core.exceptions import ValidationError
Expand All @@ -36,7 +38,7 @@ class MethodForm(forms.Form):

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields["method"].choices = [
self.fields["method"].choices = [ # type: ignore[attr-defined]
(backend.name, backend.verbose) for backend in list_backends()
]

Expand Down
8 changes: 6 additions & 2 deletions weblate_web/management/commands/recurring_payments.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@

from __future__ import annotations

from datetime import timedelta
from datetime import datetime, timedelta
from typing import TYPE_CHECKING

from django.conf import settings
from django.core.management.base import BaseCommand
Expand All @@ -29,6 +30,9 @@
from weblate_web.payments.models import Payment
from weblate_web.payments.utils import send_notification

if TYPE_CHECKING:
from collections.abc import Iterable


class Command(BaseCommand):
help = "issues recurring payments"
Expand All @@ -46,7 +50,7 @@ def handle(self, *args, **options):

@staticmethod
def notify_expiry(weekday=0):
expiry = []
expiry: list[tuple[str, Iterable[str], datetime]] = []

expires_notify = timezone.now() + timedelta(days=30)
payment_notify_start = timezone.now() + timedelta(days=7)
Expand Down
2 changes: 1 addition & 1 deletion weblate_web/management/commands/zammad_spam.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ def handle(self, *args, **options):
data = zammad.ticket._raise_or_return_json(response)

# Upload to IMAP
imap.append(settings.IMAP_SPAM_FOLDER, None, None, data)
imap.append(settings.IMAP_SPAM_FOLDER, "", "", data)

# Add tag
tag_obj.add("Ticket", ticket_id, "reported-spam")
9 changes: 6 additions & 3 deletions weblate_web/management/commands/zammad_sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,15 @@ def handle(self, *args, **options):

def handle_hosted_account(self):
"""Define link to search account on Hosted Weblate for all users."""
self.client.user.per_page = 100
users = self.client.user.search(
self.client.user.per_page = 100 # type: ignore[union-attr]
users = self.client.user.search( # type: ignore[union-attr]
{"query": f"!hosted_account:{HOSTED_ACCOUNT!r}", "limit": 100}
)
# We intentionally ignore pagination here as the sync is expected to run
# regularly and fetch remaining ones in next run
for user in users:
self.client.user.update(user["id"], {"hosted_account": HOSTED_ACCOUNT})
self.client.user.update( # type: ignore[union-attr]
user["id"],
{"hosted_account": HOSTED_ACCOUNT},
)
self.stdout.write(f"Updating {user['login']}")
30 changes: 18 additions & 12 deletions weblate_web/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@
from uuid import uuid4

import html2text
import PIL
import requests
from django.conf import settings
from django.contrib.auth.models import User
Expand All @@ -43,6 +42,7 @@
from django_countries import countries
from markupfield.fields import MarkupField
from paramiko.client import SSHClient
from PIL import Image as PILImage

from weblate_web.payments.fields import Char32UUIDField
from weblate_web.payments.models import Customer, Payment, get_period_delta
Expand Down Expand Up @@ -120,13 +120,15 @@ def validate_bitmap(value):
try:
# load() could spot a truncated JPEG, but it loads the entire
# image in memory, which is a DoS vector. See #3848 and #18520.
image = PIL.Image.open(content)
image = PILImage.open(content)
# verify() must be called immediately after the constructor.
image.verify()

# Pillow doesn't detect the MIME type of all formats. In those
# cases, content_type will be None.
value.file.content_type = PIL.Image.MIME.get(image.format)
value.file.content_type = PILImage.MIME.get(
image.format # type: ignore[arg-type]
)
except Exception as error:
# Pillow doesn't recognize it as an image.
raise ValidationError(_("Invalid image!"), code="invalid_image").with_traceback(
Expand Down Expand Up @@ -445,7 +447,7 @@ class Meta:
def __str__(self):
return self.title

def save(
def save( # type: ignore[override]
self,
*,
force_insert: bool = False,
Expand Down Expand Up @@ -566,6 +568,10 @@ class Service(models.Model):
],
)

# Discover integration
matched_projects: list[Project]
non_matched_projects_count: int

class Meta:
verbose_name = "Customer service"
verbose_name_plural = "Customer services"
Expand Down Expand Up @@ -598,7 +604,7 @@ def projects_limit(self):
return f"{report.projects}"
return "0"

projects_limit.short_description = "Projects"
projects_limit.short_description = "Projects" # type: ignore[attr-defined]

def languages_limit(self):
report = self.last_report
Expand All @@ -608,7 +614,7 @@ def languages_limit(self):
return f"{report.languages}"
return "0"

languages_limit.short_description = "Languages"
languages_limit.short_description = "Languages" # type: ignore[attr-defined]

def source_strings_limit(self):
report = self.last_report
Expand All @@ -618,7 +624,7 @@ def source_strings_limit(self):
return f"{report.source_strings}"
return "0"

source_strings_limit.short_description = "Source strings"
source_strings_limit.short_description = "Source strings" # type: ignore[attr-defined]

def hosted_words_limit(self):
report = self.last_report
Expand All @@ -628,7 +634,7 @@ def hosted_words_limit(self):
return f"{report.hosted_words}"
return "0"

hosted_words_limit.short_description = "Hosted words"
hosted_words_limit.short_description = "Hosted words" # type: ignore[attr-defined]

def hosted_strings_limit(self):
report = self.last_report
Expand All @@ -638,7 +644,7 @@ def hosted_strings_limit(self):
return f"{report.hosted_strings}"
return "0"

hosted_strings_limit.short_description = "Hosted strings"
hosted_strings_limit.short_description = "Hosted strings" # type: ignore[attr-defined]

@cached_property
def user_emails(self):
Expand Down Expand Up @@ -873,7 +879,7 @@ class Meta:
def __str__(self):
return f"{self.package}: {self.service}"

def save(
def save( # type: ignore[override]
self,
*,
force_insert: bool = False,
Expand Down Expand Up @@ -924,7 +930,7 @@ def send_notification(self, notification):
emails = {user.email for user in self.service.users.all()}
if self.payment_obj.customer.email:
emails.add(self.payment_obj.customer.email)
send_notification(notification, emails, subscription=self)
send_notification(notification, list(emails), subscription=self)
with override("en"):
send_notification(
notification,
Expand Down Expand Up @@ -1028,7 +1034,7 @@ class Meta:
def __str__(self):
return self.site_url

def save(
def save( # type: ignore[override]
self,
*,
force_insert: bool = False,
Expand Down
5 changes: 2 additions & 3 deletions weblate_web/payments/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@
#

import json
from copy import copy
from datetime import date

import responses
Expand Down Expand Up @@ -263,9 +262,9 @@ def test_proforma(self):
self.assertEqual(mail.outbox[0].subject, "Your pending payment on weblate.org")
mail.outbox = []

received = copy(FIO_TRASACTIONS)
received = FIO_TRASACTIONS.copy()
proforma_id = backend.payment.invoice
transaction = received["accountStatement"]["transactionList"]["transaction"]
transaction = received["accountStatement"]["transactionList"]["transaction"] # type: ignore[index]
transaction[0]["column16"]["value"] = proforma_id
transaction[1]["column16"]["value"] = proforma_id
transaction[1]["column1"]["value"] = backend.payment.amount * 1.21
Expand Down
21 changes: 20 additions & 1 deletion weblate_web/remote.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
from __future__ import annotations

import operator
from typing import Literal, TypedDict

import requests
import sentry_sdk
Expand All @@ -37,6 +38,24 @@
CACHE_TIMEOUT = 72 * 3600


class PYPIInfo(TypedDict):
comment_text: str
digests: dict[Literal["blake2b_256", "md5", "sha256"], str]
downloads: int
filename: str
has_sig: bool
md5_digest: str
packagetype: str
python_version: str
requires_python: str
size: int
upload_time: str
upload_time_iso_8601: str
url: str
yanked: bool
yanked_reason: None | str


def get_contributors(force: bool = False):
key = "wlweb-contributors"
results = cache.get(key)
Expand Down Expand Up @@ -113,7 +132,7 @@ def get_changes(force: bool = False):
return stats[:10]


def get_release(force: bool = False) -> None | list[dict[str, str]]:
def get_release(force: bool = False) -> None | list[PYPIInfo]:
key = "wlweb-release-x"
results = cache.get(key)
if not force and results is not None:
Expand Down
10 changes: 10 additions & 0 deletions weblate_web/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -427,6 +427,16 @@
THEPAY_SERVER: str
THEPAY_PROJECT_ID: str

# API authentication
PAYMENT_SECRET: str
ZAMMAD_TOKEN: str

# IMAP
IMAP_SERVER: str
IMAP_USER: str
IMAP_PASSWORD: str
IMAP_SPAM_FOLDER: str

LOCAL = Path(BASE_DIR) / "weblate_web" / "settings_local.py"
if LOCAL.exists():
local_settings = LOCAL.read_text()
Expand Down
12 changes: 9 additions & 3 deletions weblate_web/templatetags/downloads.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,21 @@
# along with this program. If not, see <https://www.gnu.org/licenses/>.
#

from __future__ import annotations

from typing import TYPE_CHECKING

from django.template import Library
from django.utils.translation import gettext as _
from django.utils.translation import ngettext

if TYPE_CHECKING:
from weblate_web.remote import PYPIInfo

register = Library()


def filesizeformat(num_bytes: int):
def filesizeformat(num_bytes: int) -> str:
"""
Format the value like a 'human-readable' file size.
Expand All @@ -43,7 +49,7 @@ def filesizeformat(num_bytes: int):


@register.inclusion_tag("snippets/download-link.html")
def downloadlink(info: dict[str, str]):
def downloadlink(info: PYPIInfo) -> dict[str, str]:
name = info["filename"]

if name.endswith(".tar.bz2"):
Expand All @@ -59,7 +65,7 @@ def downloadlink(info: dict[str, str]):
else:
text = name

size = filesizeformat(info["size"])
size = filesizeformat(int(info["size"]))

return {
"url": info["url"],
Expand Down
4 changes: 4 additions & 0 deletions weblate_web/templatetags/site_url.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,14 @@ def add_site_url(content):
tree = etree.parse(StringIO(content), parser) # noqa: S320
for link in tree.findall(".//a"):
url = link.get("href")
if url is None:
continue
if url.startswith("/"):
link.set("href", "https://weblate.org" + url)
for link in tree.findall(".//img"):
url = link.get("src")
if url is None:
continue
if url.startswith("/"):
link.set("src", "https://weblate.org" + url)
return mark_safe( # noqa: S308
Expand Down
Loading

0 comments on commit 2442a7b

Please sign in to comment.