Fix types

This commit is contained in:
Joakim Hellsén 2026-03-16 18:40:04 +01:00
commit c092d3089f
Signed by: Joakim Hellsén
SSH key fingerprint: SHA256:/9h/CsExpFp+PRhsfA0xznFx2CGfTT5R/kpuFfUgEQk
13 changed files with 48 additions and 18 deletions

View file

@ -12,7 +12,6 @@ from config import settings
if TYPE_CHECKING: if TYPE_CHECKING:
from collections.abc import Callable from collections.abc import Callable
from collections.abc import Generator from collections.abc import Generator
from collections.abc import Iterator
from pathlib import Path from pathlib import Path
from types import ModuleType from types import ModuleType
@ -28,7 +27,7 @@ def reload_settings_module() -> Generator[Callable[..., ModuleType]]:
original_env: dict[str, str] = os.environ.copy() original_env: dict[str, str] = os.environ.copy()
@contextmanager @contextmanager
def temporary_env(env: dict[str, str]) -> Iterator[None]: def temporary_env(env: dict[str, str]) -> Generator[None]:
previous_env: dict[str, str] = os.environ.copy() previous_env: dict[str, str] = os.environ.copy()
os.environ.clear() os.environ.clear()
os.environ.update(env) os.environ.update(env)

View file

@ -53,9 +53,9 @@ if TYPE_CHECKING:
from os import stat_result from os import stat_result
from pathlib import Path from pathlib import Path
from debug_toolbar.utils import QueryDict
from django.db.models import QuerySet from django.db.models import QuerySet
from django.http import HttpRequest from django.http import HttpRequest
from django.http.request import QueryDict
logger: logging.Logger = logging.getLogger("ttvdrops.views") logger: logging.Logger = logging.getLogger("ttvdrops.views")
@ -309,7 +309,7 @@ def docs_rss_view(request: HttpRequest) -> HttpResponse:
# Add limit=1 to GET parameters # Add limit=1 to GET parameters
get_data: QueryDict = request.GET.copy() get_data: QueryDict = request.GET.copy()
get_data["limit"] = "1" get_data["limit"] = "1"
limited_request.GET = get_data # pyright: ignore[reportAttributeAccessIssue] limited_request.GET = get_data
response: HttpResponse = feed_view(limited_request, *args) response: HttpResponse = feed_view(limited_request, *args)
return _pretty_example(response.content.decode("utf-8")) return _pretty_example(response.content.decode("utf-8"))

View file

@ -289,7 +289,7 @@ class KickDropCampaign(auto_prefetch.Model):
"""Return the image URL for the campaign.""" """Return the image URL for the campaign."""
# Image from first drop # Image from first drop
if self.rewards.exists(): # pyright: ignore[reportAttributeAccessIssue] if self.rewards.exists(): # pyright: ignore[reportAttributeAccessIssue]
first_reward: KickReward = self.rewards.first() # pyright: ignore[reportAttributeAccessIssue] first_reward: KickReward | None = self.rewards.first() # pyright: ignore[reportAttributeAccessIssue]
if first_reward and first_reward.image_url: if first_reward and first_reward.image_url:
return first_reward.full_image_url return first_reward.full_image_url

View file

@ -442,6 +442,8 @@ class ImportKickDropsCommandTest(TestCase):
campaign: KickDropCampaign = KickDropCampaign.objects.get() campaign: KickDropCampaign = KickDropCampaign.objects.get()
assert campaign.name == "PUBG 9th Anniversary" assert campaign.name == "PUBG 9th Anniversary"
assert campaign.status == "active" assert campaign.status == "active"
assert campaign.organization is not None
assert campaign.category is not None
assert campaign.organization.name == "KRAFTON" assert campaign.organization.name == "KRAFTON"
assert campaign.category.name == "PUBG: Battlegrounds" assert campaign.category.name == "PUBG: Battlegrounds"

View file

@ -177,7 +177,9 @@ class TTVDropsAtomBaseFeed(TTVDropsBaseFeed):
feed_type = BrowserFriendlyAtom1Feed feed_type = BrowserFriendlyAtom1Feed
def _with_campaign_related(queryset: QuerySet[DropCampaign]) -> QuerySet[DropCampaign]: def _with_campaign_related(
queryset: QuerySet[DropCampaign, DropCampaign],
) -> QuerySet[DropCampaign, DropCampaign]:
"""Apply related-selects/prefetches needed by feed rendering to avoid N+1 queries. """Apply related-selects/prefetches needed by feed rendering to avoid N+1 queries.
Returns: Returns:

View file

@ -631,6 +631,9 @@ class Command(BaseCommand):
) )
return return
if game_obj.box_art_file is None:
return
game_obj.box_art_file.save(file_name, ContentFile(response.content), save=True) game_obj.box_art_file.save(file_name, ContentFile(response.content), save=True)
def _get_or_create_channel(self, channel_info: ChannelInfoSchema) -> Channel: def _get_or_create_channel(self, channel_info: ChannelInfoSchema) -> Channel:

View file

@ -11,8 +11,8 @@ from twitch.models import Game
from twitch.models import Organization from twitch.models import Organization
if TYPE_CHECKING: if TYPE_CHECKING:
from debug_toolbar.panels.templates.panel import QuerySet
from django.core.management.base import CommandParser from django.core.management.base import CommandParser
from django.db.models import QuerySet
class Command(BaseCommand): class Command(BaseCommand):

View file

@ -38,7 +38,7 @@ class Command(BaseCommand):
help="Re-download even if a local box art file already exists.", help="Re-download even if a local box art file already exists.",
) )
def handle(self, *_args: object, **options: object) -> None: def handle(self, *_args: object, **options: object) -> None: # noqa: PLR0914
"""Download Twitch box art images for all games.""" """Download Twitch box art images for all games."""
limit_value: object | None = options.get("limit") limit_value: object | None = options.get("limit")
limit: int | None = limit_value if isinstance(limit_value, int) else None limit: int | None = limit_value if isinstance(limit_value, int) else None
@ -92,6 +92,10 @@ class Command(BaseCommand):
skipped += 1 skipped += 1
continue continue
if game.box_art_file is None:
failed += 1
continue
game.box_art_file.save( game.box_art_file.save(
file_name, file_name,
ContentFile(response.content), ContentFile(response.content),
@ -99,7 +103,9 @@ class Command(BaseCommand):
) )
# Auto-convert to WebP and AVIF # Auto-convert to WebP and AVIF
self._convert_to_modern_formats(game.box_art_file.path) box_art_path: str | None = getattr(game.box_art_file, "path", None)
if box_art_path:
self._convert_to_modern_formats(box_art_path)
downloaded += 1 downloaded += 1

View file

@ -264,7 +264,7 @@ class Command(BaseCommand):
client: httpx.Client, client: httpx.Client,
image_url: str, image_url: str,
twitch_id: str, twitch_id: str,
file_field: FieldFile, file_field: FieldFile | None,
) -> str: ) -> str:
"""Download a single image and save it to the file field. """Download a single image and save it to the file field.
@ -281,6 +281,9 @@ class Command(BaseCommand):
suffix: str = Path(parsed_url.path).suffix or ".jpg" suffix: str = Path(parsed_url.path).suffix or ".jpg"
file_name: str = f"{twitch_id}{suffix}" file_name: str = f"{twitch_id}{suffix}"
if file_field is None:
return "failed"
try: try:
response: httpx.Response = client.get(image_url) response: httpx.Response = client.get(image_url)
response.raise_for_status() response.raise_for_status()
@ -299,7 +302,9 @@ class Command(BaseCommand):
file_field.save(file_name, ContentFile(response.content), save=True) file_field.save(file_name, ContentFile(response.content), save=True)
# Auto-convert to WebP and AVIF # Auto-convert to WebP and AVIF
self._convert_to_modern_formats(file_field.path) image_path: str | None = getattr(file_field, "path", None)
if image_path:
self._convert_to_modern_formats(image_path)
return "downloaded" return "downloaded"

View file

@ -279,6 +279,7 @@ class RSSFeedTestCase(TestCase):
def test_campaign_and_game_feeds_use_absolute_media_enclosure_urls(self) -> None: def test_campaign_and_game_feeds_use_absolute_media_enclosure_urls(self) -> None:
"""Campaign/game RSS+Atom enclosures should use absolute URLs for local media files.""" """Campaign/game RSS+Atom enclosures should use absolute URLs for local media files."""
self.game.box_art = "" self.game.box_art = ""
assert self.game.box_art_file is not None
self.game.box_art_file.save( self.game.box_art_file.save(
"box.png", "box.png",
ContentFile(b"game-image-bytes"), ContentFile(b"game-image-bytes"),
@ -289,6 +290,7 @@ class RSSFeedTestCase(TestCase):
self.game.save() self.game.save()
self.campaign.image_url = "" self.campaign.image_url = ""
assert self.campaign.image_file is not None
self.campaign.image_file.save( self.campaign.image_file.save(
"campaign.png", "campaign.png",
ContentFile(b"campaign-image-bytes"), ContentFile(b"campaign-image-bytes"),
@ -712,6 +714,7 @@ class RSSFeedTestCase(TestCase):
name="File Game", name="File Game",
display_name="File Game", display_name="File Game",
) )
assert game2.box_art_file is not None
game2.box_art_file.save("sample.png", ContentFile(b"hello")) game2.box_art_file.save("sample.png", ContentFile(b"hello"))
game2.save() game2.save()
@ -723,6 +726,7 @@ class RSSFeedTestCase(TestCase):
end_at=timezone.now() + timedelta(days=1), end_at=timezone.now() + timedelta(days=1),
operation_names=["DropCampaignDetails"], operation_names=["DropCampaignDetails"],
) )
assert campaign2.image_file is not None
campaign2.image_file.save("camp.jpg", ContentFile(b"world")) campaign2.image_file.save("camp.jpg", ContentFile(b"world"))
campaign2.save() campaign2.save()

View file

@ -1067,9 +1067,18 @@ class TestSEOHelperFunctions:
def test_build_seo_context_with_all_parameters(self) -> None: def test_build_seo_context_with_all_parameters(self) -> None:
"""Test _build_seo_context with all parameters.""" """Test _build_seo_context with all parameters."""
now: datetime.datetime = timezone.now() now: datetime.datetime = timezone.now()
breadcrumb: list[dict[str, int | str]] = [ breadcrumb: dict[str, Any] = {
{"position": 1, "name": "Home", "url": "/"}, "@context": "https://schema.org",
] "@type": "BreadcrumbList",
"itemListElement": [
{
"@type": "ListItem",
"position": 1,
"name": "Home",
"item": "/",
},
],
}
context: dict[str, Any] = _build_seo_context( context: dict[str, Any] = _build_seo_context(
page_title="Test", page_title="Test",
@ -1077,7 +1086,7 @@ class TestSEOHelperFunctions:
page_image="https://example.com/img.jpg", page_image="https://example.com/img.jpg",
og_type="article", og_type="article",
schema_data={}, schema_data={},
breadcrumb_schema=breadcrumb, # pyright: ignore[reportArgumentType] breadcrumb_schema=breadcrumb,
pagination_info=[{"rel": "next", "url": "/page/2/"}], pagination_info=[{"rel": "next", "url": "/page/2/"}],
published_date=now.isoformat(), published_date=now.isoformat(),
modified_date=now.isoformat(), modified_date=now.isoformat(),

View file

@ -53,7 +53,7 @@ def normalize_twitch_box_art_url(url: str) -> str:
return url return url
normalized_path: str = TWITCH_BOX_ART_SIZE_PATTERN.sub("", parsed.path) normalized_path: str = TWITCH_BOX_ART_SIZE_PATTERN.sub("", parsed.path)
return urlunparse(parsed._replace(path=normalized_path)) return str(urlunparse(parsed._replace(path=normalized_path)))
@lru_cache(maxsize=40 * 40 * 1024) @lru_cache(maxsize=40 * 40 * 1024)

View file

@ -1396,7 +1396,7 @@ def reward_campaign_detail_view(request: HttpRequest, twitch_id: str) -> HttpRes
class GamesListView(GamesGridView): class GamesListView(GamesGridView):
"""List view for games in simple list format.""" """List view for games in simple list format."""
template_name: str = "twitch/games_list.html" template_name: str | None = "twitch/games_list.html"
# MARK: /channels/ # MARK: /channels/
@ -1748,7 +1748,7 @@ def badge_set_detail_view(request: HttpRequest, set_id: str) -> HttpResponse:
) )
return ChatBadge.objects.filter(pk__in=badge_ids).order_by(preserved_order) return ChatBadge.objects.filter(pk__in=badge_ids).order_by(preserved_order)
badges = get_sorted_badges(badge_set) badges: QuerySet[ChatBadge, ChatBadge] = get_sorted_badges(badge_set)
# Attach award_campaigns attribute to each badge for template use # Attach award_campaigns attribute to each badge for template use
for badge in badges: for badge in badges: