From 66accd4a9686d6c0cf3b5c93e8ddc0db7e554640 Mon Sep 17 00:00:00 2001 From: Jelle van der Waa Date: Sat, 19 Sep 2026 16:58:05 +0200 Subject: [PATCH 1/4] sitemaps: Type all methods returning None --- sitemaps.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/sitemaps.py b/sitemaps.py index 9dc5a50a..5f9151b6 100644 --- a/sitemaps.py +++ b/sitemaps.py @@ -20,7 +20,7 @@ def items(self): def lastmod(self, obj): return obj.last_update - def get_latest_lastmod(self): + def get_latest_lastmod(self) -> None: return None def changefreq(self, obj): @@ -49,7 +49,7 @@ def location(self, obj): def lastmod(self, obj): return obj.files_last_update - def get_latest_lastmod(self): + def get_latest_lastmod(self) -> None: return None @@ -63,7 +63,7 @@ def items(self): def lastmod(self, obj): return obj['last_update'] - def get_latest_lastmod(self): + def get_latest_lastmod(self) -> None: return None def location(self, obj): @@ -80,7 +80,7 @@ def items(self): def lastmod(self, obj): return obj['last_update'] - def get_latest_lastmod(self): + def get_latest_lastmod(self) -> None: return None def location(self, obj): @@ -99,7 +99,7 @@ def items(self): def lastmod(self, obj): return obj.last_modified - def get_latest_lastmod(self): + def get_latest_lastmod(self) -> None: return None def priority(self, obj): @@ -131,7 +131,7 @@ def items(self): def lastmod(self, obj): return obj.last_modified - def get_latest_lastmod(self): + def get_latest_lastmod(self) -> None: return None def priority(self, obj): @@ -153,7 +153,7 @@ def items(self): def lastmod(self, obj): return obj.last_modified - def get_latest_lastmod(self): + def get_latest_lastmod(self) -> None: return None def changefreq(self, obj): From 9e31d73c11355d0c203cd3b88ec13dfec3713e44 Mon Sep 17 00:00:00 2001 From: Jelle van der Waa Date: Sat, 19 Sep 2026 19:25:10 +0200 Subject: [PATCH 2/4] releng: Add more type hints --- releng/models.py | 36 +++++++++++++++++++++++++----------- releng/tests/conftest.py | 5 +++-- releng/tests/test_models.py | 27 +++++++++++++++++---------- releng/tests/test_views.py | 19 ++++++++++++------- releng/views.py | 12 ++++++------ 5 files changed, 63 insertions(+), 36 deletions(-) diff --git a/releng/models.py b/releng/models.py index 9214aea0..30024a6b 100644 --- a/releng/models.py +++ b/releng/models.py @@ -2,17 +2,31 @@ import hashlib from base64 import b64decode from datetime import datetime, timezone +from typing import TypedDict from bencode import bdecode, bencode from django.db import models from django.db.models.signals import pre_save from django.urls import reverse -from django.utils.safestring import mark_safe +from django.utils.safestring import SafeText, mark_safe from devel.fields import PGPKeyField from main.utils import parse_markdown, set_created_field +class TorrentMetadata(TypedDict): + comment: str | None + created_by: str | None + creation_date: datetime | None + announce: str | None + file_name: str | None + file_length: int | None + piece_count: float | None + piece_length: int | None + url_list: list[str] | None + info_hash: str | None + + class Release(models.Model): release_date = models.DateField(db_index=True) version = models.CharField(max_length=50, unique=True) @@ -34,25 +48,25 @@ class Meta: get_latest_by = 'release_date' ordering = ('-release_date', '-version') - def __str__(self): + def __str__(self) -> str: return self.version - def get_absolute_url(self): + def get_absolute_url(self) -> str: return reverse('releng-release-detail', args=[self.version]) - def dir_path(self): + def dir_path(self) -> str: return "iso/%s/" % self.version - def iso_url(self): + def iso_url(self) -> str: return f"iso/{self.version}/archlinux-{self.version}-x86_64.iso" - def tarball_url(self): + def tarball_url(self) -> str: return f"iso/{self.version}/archlinux-bootstrap-{self.version}-x86_64.tar.zst" - def dir_url(self): + def dir_url(self) -> str: return "iso/%s" % (self.version) - def magnet_uri(self): + def magnet_uri(self) -> str: query = [ ('dn', "archlinux-%s-x86_64.iso" % self.version), ] @@ -61,10 +75,10 @@ def magnet_uri(self): query.insert(0, ('xt', "urn:btih:%s" % metadata['info_hash'])) return "magnet:?%s" % '&'.join([f'{k}={v}' for k, v in query]) - def info_html(self): + def info_html(self) -> SafeText: return mark_safe(parse_markdown(self.info)) - def torrent(self): + def torrent(self) -> TorrentMetadata | None: try: data = b64decode(self.torrent_data.encode('utf-8')) except (TypeError, binascii.Error): @@ -74,7 +88,7 @@ def torrent(self): data = bdecode(data) # transform the data into a template-friendly dict info = data.get('info', {}) - metadata = { + metadata: TorrentMetadata = { 'comment': data.get('comment', None), 'created_by': data.get('created by', None), 'creation_date': None, diff --git a/releng/tests/conftest.py b/releng/tests/conftest.py index 7247fd46..373ec859 100644 --- a/releng/tests/conftest.py +++ b/releng/tests/conftest.py @@ -1,4 +1,5 @@ from base64 import b64encode +from collections.abc import Generator from datetime import datetime, timezone import pytest @@ -15,7 +16,7 @@ @pytest.fixture -def release(db): +def release(db: None) -> Generator[Release, None, None]: release = Release.objects.create( release_date=datetime.now(), version=VERSION, @@ -28,7 +29,7 @@ def release(db): @pytest.fixture -def torrent_data(): +def torrent_data() -> str: data = { 'comment': 'comment', 'created_by': 'Arch Linux', diff --git a/releng/tests/test_models.py b/releng/tests/test_models.py index 2d5701d3..c7fe29b9 100644 --- a/releng/tests/test_models.py +++ b/releng/tests/test_models.py @@ -1,50 +1,57 @@ -def test_feed(client, release): +from django.test import Client + +from releng.models import Release + + +def test_feed(client: Client, release: Release) -> None: response = client.get('/feeds/releases/') assert response.status_code == 200 -def test_str(release): +def test_str(release: Release) -> None: assert str(release) == release.version -def test_absolute_url(release): +def test_absolute_url(release: Release) -> None: assert release.version, release.get_absolute_url() -def test_iso_url(release): +def test_iso_url(release: Release) -> None: url = release.iso_url() ver = release.version expected = f'iso/{ver}/archlinux-{ver}-x86_64.iso' assert url == expected -def test_info_html(release): +def test_info_html(release: Release) -> None: assert release.info in release.info_html() -def test_dir_path(release): +def test_dir_path(release: Release) -> None: dir_path = f'iso/{release.version}/' assert dir_path == release.dir_path() -def test_sitemap(client, release): +def test_sitemap(client: Client, release: Release) -> None: response = client.get('/sitemap-releases.xml') assert response.status_code == 200 -def test_garbage_torrent_data(release): +def test_garbage_torrent_data(release: Release) -> None: assert release.torrent() is None release.torrent_data = 'garbage' assert release.torrent() is None -def test_torrent_data(release, torrent_data): +def test_torrent_data(release: Release, torrent_data: str) -> None: release.torrent_data = torrent_data data = release.torrent() + assert data is not None + assert data['file_name'] is not None assert 'arch' in data['file_name'] -def test_magnet_uri(release, torrent_data): +def test_magnet_uri(release: Release, torrent_data: str) -> None: release.torrent_data = torrent_data assert release.magnet_uri() diff --git a/releng/tests/test_views.py b/releng/tests/test_views.py index f71f22d4..63a9c9d3 100644 --- a/releng/tests/test_views.py +++ b/releng/tests/test_views.py @@ -1,4 +1,9 @@ -def test_release_json(client, release, torrent_data): +from django.test import Client + +from releng.models import Release + + +def test_release_json(client: Client, release: Release, torrent_data: str) -> None: version = release.version response = client.get('/releng/releases/json/') assert response.status_code == 200 @@ -20,7 +25,7 @@ def test_release_json(client, release, torrent_data): assert release_data['wkd_email'] == release.wkd_email -def test_json(db, client): +def test_json(db: None, client: Client) -> None: response = client.get('/releng/releases/json/') assert response.status_code == 200 @@ -28,7 +33,7 @@ def test_json(db, client): assert data['releases'] == [] -def test_release_json_null_pgp_fingerprint_and_wkd_email(client, db): +def test_release_json_null_pgp_fingerprint_and_wkd_email(client: Client, db: None) -> None: from datetime import datetime from releng.models import Release @@ -45,17 +50,17 @@ def test_release_json_null_pgp_fingerprint_and_wkd_email(client, db): assert release_data['wkd_email'] is None -def test_netboot_page(db, client): +def test_netboot_page(db: None, client: Client) -> None: response = client.get('/releng/netboot/') assert response.status_code == 200 -def test_netboot_config(db, client): +def test_netboot_config(db: None, client: Client): response = client.get('/releng/netboot/archlinux.ipxe') assert response.status_code == 200 -def test_release_torrent(client, release, torrent_data): +def test_release_torrent(client: Client, release: Release, torrent_data: str) -> None: response = client.get(f'/releng/releases/{release.version}/torrent/') assert response.status_code == 404 @@ -65,7 +70,7 @@ def test_release_torrent(client, release, torrent_data): assert response.status_code == 200 -def test_release_details(client, release): +def test_release_details(client: Client, release: Release) -> None: response = client.get(f'/releng/releases/{release.version}/') assert response.status_code == 200 assert release.version in response.content.decode() diff --git a/releng/views.py b/releng/views.py index d6d833b2..00cbced1 100644 --- a/releng/views.py +++ b/releng/views.py @@ -3,7 +3,7 @@ from django.conf import settings from django.core.serializers.json import DjangoJSONEncoder -from django.http import Http404, HttpResponse +from django.http import Http404, HttpRequest, HttpResponse from django.shortcuts import get_object_or_404, render from django.urls import reverse from django.views.generic import DetailView, ListView @@ -25,7 +25,7 @@ class ReleaseDetailView(DetailView): slug_url_kwarg = 'version' -def release_torrent(request, version): +def release_torrent(request: HttpRequest, version: str) -> HttpResponse: release = get_object_or_404(Release, version=version) if not release.torrent_data: raise Http404 @@ -61,7 +61,7 @@ def default(self, obj): @deprecated_json_endpoint('/api/v1/releng/releases/') -def releases_json(request): +def releases_json(request: HttpRequest) -> HttpResponse: releases = Release.objects.all() try: latest_version = Release.objects.filter(available=True).values_list( @@ -95,10 +95,10 @@ def netboot_config(request): return render(request, "releng/archlinux.ipxe", context, content_type='text/plain') -def netboot_info(request): +def netboot_info(request: HttpRequest) -> HttpResponse: ipxepkg = None - ipxepkgs = Package.objects.filter(pkgname='ipxe').all() - ipxepkgs = [pkg for pkg in ipxepkgs if not pkg.repo.testing and 'Staging' not in pkg.repo.name] + ipxepkgs_qs = Package.objects.filter(pkgname='ipxe').all() + ipxepkgs = [pkg for pkg in ipxepkgs_qs if not pkg.repo.testing and 'Staging' not in pkg.repo.name] if ipxepkgs: ipxepkg = ipxepkgs[0] From f615b1143797c31f4257199bdc6c7cd09ab91077 Mon Sep 17 00:00:00 2001 From: Jelle van der Waa Date: Sat, 19 Sep 2026 19:44:26 +0200 Subject: [PATCH 3/4] releng: Rename django queryset Re-using mirrorurls causes typing issues as it changes the QuerySet to a list. --- releng/views.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/releng/views.py b/releng/views.py index 00cbced1..04a99773 100644 --- a/releng/views.py +++ b/releng/views.py @@ -81,12 +81,12 @@ def releases_json(request: HttpRequest) -> HttpResponse: def netboot_config(request): releases = Release.objects.filter(available=True).values_list('version', flat=True).order_by('-release_date') - mirrorurls = MirrorUrl.objects.filter(protocol__protocol='http', - active=True, - mirror__public=True, - mirror__active=True, - mirror__isos=True) - mirrorurls = sorted(mirrorurls, key=lambda x: x.mirror.name) + mirrorurls_qs = MirrorUrl.objects.filter(protocol__protocol='http', + active=True, + mirror__public=True, + mirror__active=True, + mirror__isos=True) + mirrorurls = sorted(mirrorurls_qs, key=lambda x: x.mirror.name) mirrorurls = sorted(mirrorurls, key=lambda x: x.country.name) context = { 'releases': releases, From f1a22e489df17255f1119a56de05fd9748633c40 Mon Sep 17 00:00:00 2001 From: Jelle van der Waa Date: Sun, 20 Sep 2026 15:07:02 +0200 Subject: [PATCH 4/4] treewide: Add trivial type hints everywhere --- devel/management/commands/generate_keyring.py | 2 +- devel/models.py | 8 ++--- devel/reports.py | 2 +- devel/tests/test_pgp_import.py | 2 +- devel/tests/test_reports.py | 35 ++++++++++--------- devel/tests/test_retire_user.py | 10 +++--- devel/utils.py | 2 +- feeds.py | 2 +- main/log.py | 2 +- main/models.py | 26 +++++++------- main/tests/test_donor_import.py | 8 ++--- main/tests/test_templatetags_pgp.py | 6 ++-- main/utils.py | 2 +- mirrors/models.py | 27 +++++++------- mirrors/tests/test_templatetags.py | 6 ++-- news/models.py | 8 ++--- news/tests/test_models.py | 11 +++--- news/views.py | 6 ++-- packages/alpm.py | 4 +-- .../management/commands/populate_signoffs.py | 4 +-- packages/models.py | 26 +++++++------- packages/tests/test_alpm.py | 8 ++--- packages/tests/test_flag_packages.py | 9 ++--- packages/utils.py | 2 +- planet/models.py | 8 ++--- planet/tests/test_views.py | 5 +-- sitemaps.py | 4 +-- todolists/models.py | 12 +++---- visualize/tests/test_urls.py | 5 ++- visualize/views.py | 8 ++--- 30 files changed, 136 insertions(+), 124 deletions(-) diff --git a/devel/management/commands/generate_keyring.py b/devel/management/commands/generate_keyring.py index 0ce5f3e7..aceef994 100644 --- a/devel/management/commands/generate_keyring.py +++ b/devel/management/commands/generate_keyring.py @@ -76,7 +76,7 @@ def generate_keyring(keyserver, keyring): } -def generate_ownertrust(trust_path): +def generate_ownertrust(trust_path: str) -> None: master_key_ids = MasterKey.objects.values_list("pgp_key", flat=True) with open(trust_path, "w") as trustfile: for key_id in master_key_ids: diff --git a/devel/models.py b/devel/models.py index bfe9f452..8714d533 100644 --- a/devel/models.py +++ b/devel/models.py @@ -66,14 +66,14 @@ class Meta: verbose_name = 'additional profile data' verbose_name_plural = 'additional profile data' - def get_absolute_url(self): + def get_absolute_url(self) -> str | None: user = self.user group = StaffGroup.objects.filter(group=user.groups.all().first()).get() if group: return f'{group.get_absolute_url()}#{user.username}' return None - def __str__(self): + def __str__(self) -> str: return f"{self.user}" @@ -114,7 +114,7 @@ class Meta: ordering = ('created',) get_latest_by = 'created' - def __str__(self): + def __str__(self) -> str: return f'{self.owner.get_full_name()}, created {self.created}' @@ -145,7 +145,7 @@ class Meta: get_latest_by = 'created' verbose_name = 'PGP signature' - def __str__(self): + def __str__(self) -> str: return f'{self.signer} → {self.signee}' diff --git a/devel/reports.py b/devel/reports.py index 7ff9c2ac..9cd3ff0f 100644 --- a/devel/reports.py +++ b/devel/reports.py @@ -20,7 +20,7 @@ def __init__(self, href, title, desc): self.title = title self.desc = desc - def __str__(self): + def __str__(self) -> str: return format_html('{desc}', href=self.href, title=self.title, desc=self.desc) diff --git a/devel/tests/test_pgp_import.py b/devel/tests/test_pgp_import.py index 54f8d0a8..4b47282b 100644 --- a/devel/tests/test_pgp_import.py +++ b/devel/tests/test_pgp_import.py @@ -18,7 +18,7 @@ ] -def test_pgp_import_error(): +def test_pgp_import_error() -> None: with pytest.raises(CommandError) as e: call_command('pgp_import') assert 'keyring_path' in str(e) diff --git a/devel/tests/test_reports.py b/devel/tests/test_reports.py index c942c5dd..b5707f45 100644 --- a/devel/tests/test_reports.py +++ b/devel/tests/test_reports.py @@ -1,12 +1,15 @@ +from collections.abc import Generator + import pytest from django.contrib.auth.models import User +from django.test import Client from devel.reports import Linkify from packages.models import PackageRelation @pytest.fixture -def devel_client(client, arches, repos, package): +def devel_client(client: Client, arches: None, repos: None, package: None) -> Generator[Client]: password = 'test' user = User.objects.create_superuser('admin', 'admin@archlinux.org', @@ -21,57 +24,57 @@ def devel_client(client, arches, repos, package): user.delete() -def test_overview(devel_client): +def test_overview(devel_client: Client) -> None: response = devel_client.get('/devel/') assert response.status_code == 200 -def test_reports_old(devel_client): +def test_reports_old(devel_client: Client) -> None: response = devel_client.get('/devel/reports/old', follow=True) assert response.status_code == 200 -def test_reports_outofdate(devel_client): +def test_reports_outofdate(devel_client: Client) -> None: response = devel_client.get('/devel/reports/long-out-of-date', follow=True) assert response.status_code == 200 -def test_reports_big(devel_client): +def test_reports_big(devel_client: Client) -> None: response = devel_client.get('/devel/reports/big', follow=True) assert response.status_code == 200 -def test_reports_badcompression(devel_client): +def test_reports_badcompression(devel_client: Client) -> None: response = devel_client.get('/devel/reports/badcompression', follow=True) assert response.status_code == 200 -def test_reports_uncompressed_man(devel_client): +def test_reports_uncompressed_man(devel_client: Client) -> None: response = devel_client.get('/devel/reports/uncompressed-man', follow=True) assert response.status_code == 200 -def test_reports_uncompressed_info(devel_client): +def test_reports_uncompressed_info(devel_client: Client) -> None: response = devel_client.get('/devel/reports/uncompressed-info', follow=True) assert response.status_code == 200 -def test_reports_unneeded_orphans(devel_client): +def test_reports_unneeded_orphans(devel_client: Client) -> None: response = devel_client.get('/devel/reports/unneeded-orphans', follow=True) assert response.status_code == 200 -def test_reports_mismatched_signature(devel_client): +def test_reports_mismatched_signature(devel_client: Client) -> None: response = devel_client.get('/devel/reports/mismatched-signature', follow=True) assert response.status_code == 200 -def test_reports_signature_time(devel_client): +def test_reports_signature_time(devel_client: Client) -> None: response = devel_client.get('/devel/reports/signature-time', follow=True) assert response.status_code == 200 -def test_reports_pkgbases(devel_client): +def test_reports_pkgbases(devel_client: Client) -> None: response = devel_client.get('/devel/reports/old/pkgbases/') assert response.status_code == 200 assert response['Content-Type'] == 'text/plain' @@ -83,12 +86,12 @@ def test_reports_pkgbases_with_username(devel_client): assert response['Content-Type'] == 'text/plain' -def test_reports_pkgbases_invalid_report(devel_client): +def test_reports_pkgbases_invalid_report(devel_client: Client) -> None: response = devel_client.get('/devel/reports/nonexistent/pkgbases/') assert response.status_code == 404 -def test_report_filtered_by_maintainer(devel_client): +def test_report_filtered_by_maintainer(devel_client: Client) -> None: user = User.objects.get(username='admin') PackageRelation.objects.create( pkgbase='linux', @@ -104,7 +107,7 @@ def test_report_filtered_by_maintainer(devel_client): assert pkgbases == {'linux'} -def test_report_pkgbases_filtered_by_maintainer(devel_client): +def test_report_pkgbases_filtered_by_maintainer(devel_client: Client) -> None: user = User.objects.get(username='admin') PackageRelation.objects.create( pkgbase='linux', @@ -118,7 +121,7 @@ def test_report_pkgbases_filtered_by_maintainer(devel_client): assert response.content.decode().strip() == 'linux' -def test_linkify_escapes_html(): +def test_linkify_escapes_html() -> None: link = Linkify(href='">', title='', desc='xss') result = str(link) assert '