From c092d3089f0cd65a1e706cd6e5c799d4955c7709 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20Helle=C5=9Ben?= Date: Mon, 16 Mar 2026 18:40:04 +0100 Subject: [PATCH] Fix types --- config/tests/test_settings.py | 3 +-- core/views.py | 4 ++-- kick/models.py | 2 +- kick/tests/test_kick.py | 2 ++ twitch/feeds.py | 4 +++- .../management/commands/better_import_drops.py | 3 +++ .../commands/cleanup_unknown_organizations.py | 2 +- twitch/management/commands/download_box_art.py | 10 ++++++++-- .../commands/download_campaign_images.py | 9 +++++++-- twitch/tests/test_feeds.py | 4 ++++ twitch/tests/test_views.py | 17 +++++++++++++---- twitch/utils.py | 2 +- twitch/views.py | 4 ++-- 13 files changed, 48 insertions(+), 18 deletions(-) diff --git a/config/tests/test_settings.py b/config/tests/test_settings.py index 298dd35..1b4a5f9 100644 --- a/config/tests/test_settings.py +++ b/config/tests/test_settings.py @@ -12,7 +12,6 @@ from config import settings if TYPE_CHECKING: from collections.abc import Callable from collections.abc import Generator - from collections.abc import Iterator from pathlib import Path from types import ModuleType @@ -28,7 +27,7 @@ def reload_settings_module() -> Generator[Callable[..., ModuleType]]: original_env: dict[str, str] = os.environ.copy() @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() os.environ.clear() os.environ.update(env) diff --git a/core/views.py b/core/views.py index 05465b8..15b982e 100644 --- a/core/views.py +++ b/core/views.py @@ -53,9 +53,9 @@ if TYPE_CHECKING: from os import stat_result from pathlib import Path - from debug_toolbar.utils import QueryDict from django.db.models import QuerySet from django.http import HttpRequest + from django.http.request import QueryDict logger: logging.Logger = logging.getLogger("ttvdrops.views") @@ -309,7 +309,7 @@ def docs_rss_view(request: HttpRequest) -> HttpResponse: # Add limit=1 to GET parameters get_data: QueryDict = request.GET.copy() get_data["limit"] = "1" - limited_request.GET = get_data # pyright: ignore[reportAttributeAccessIssue] + limited_request.GET = get_data response: HttpResponse = feed_view(limited_request, *args) return _pretty_example(response.content.decode("utf-8")) diff --git a/kick/models.py b/kick/models.py index 7cef031..7f6ff01 100644 --- a/kick/models.py +++ b/kick/models.py @@ -289,7 +289,7 @@ class KickDropCampaign(auto_prefetch.Model): """Return the image URL for the campaign.""" # Image from first drop 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: return first_reward.full_image_url diff --git a/kick/tests/test_kick.py b/kick/tests/test_kick.py index 59420ef..32e1adf 100644 --- a/kick/tests/test_kick.py +++ b/kick/tests/test_kick.py @@ -442,6 +442,8 @@ class ImportKickDropsCommandTest(TestCase): campaign: KickDropCampaign = KickDropCampaign.objects.get() assert campaign.name == "PUBG 9th Anniversary" assert campaign.status == "active" + assert campaign.organization is not None + assert campaign.category is not None assert campaign.organization.name == "KRAFTON" assert campaign.category.name == "PUBG: Battlegrounds" diff --git a/twitch/feeds.py b/twitch/feeds.py index 7726345..6a8678c 100644 --- a/twitch/feeds.py +++ b/twitch/feeds.py @@ -177,7 +177,9 @@ class TTVDropsAtomBaseFeed(TTVDropsBaseFeed): 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. Returns: diff --git a/twitch/management/commands/better_import_drops.py b/twitch/management/commands/better_import_drops.py index 3fea772..26c408e 100644 --- a/twitch/management/commands/better_import_drops.py +++ b/twitch/management/commands/better_import_drops.py @@ -631,6 +631,9 @@ class Command(BaseCommand): ) return + if game_obj.box_art_file is None: + return + game_obj.box_art_file.save(file_name, ContentFile(response.content), save=True) def _get_or_create_channel(self, channel_info: ChannelInfoSchema) -> Channel: diff --git a/twitch/management/commands/cleanup_unknown_organizations.py b/twitch/management/commands/cleanup_unknown_organizations.py index 28c8efb..3f07263 100644 --- a/twitch/management/commands/cleanup_unknown_organizations.py +++ b/twitch/management/commands/cleanup_unknown_organizations.py @@ -11,8 +11,8 @@ from twitch.models import Game from twitch.models import Organization if TYPE_CHECKING: - from debug_toolbar.panels.templates.panel import QuerySet from django.core.management.base import CommandParser + from django.db.models import QuerySet class Command(BaseCommand): diff --git a/twitch/management/commands/download_box_art.py b/twitch/management/commands/download_box_art.py index e0ea78b..04eef98 100644 --- a/twitch/management/commands/download_box_art.py +++ b/twitch/management/commands/download_box_art.py @@ -38,7 +38,7 @@ class Command(BaseCommand): 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.""" limit_value: object | None = options.get("limit") limit: int | None = limit_value if isinstance(limit_value, int) else None @@ -92,6 +92,10 @@ class Command(BaseCommand): skipped += 1 continue + if game.box_art_file is None: + failed += 1 + continue + game.box_art_file.save( file_name, ContentFile(response.content), @@ -99,7 +103,9 @@ class Command(BaseCommand): ) # 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 diff --git a/twitch/management/commands/download_campaign_images.py b/twitch/management/commands/download_campaign_images.py index 4d419ca..d7063d4 100644 --- a/twitch/management/commands/download_campaign_images.py +++ b/twitch/management/commands/download_campaign_images.py @@ -264,7 +264,7 @@ class Command(BaseCommand): client: httpx.Client, image_url: str, twitch_id: str, - file_field: FieldFile, + file_field: FieldFile | None, ) -> str: """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" file_name: str = f"{twitch_id}{suffix}" + if file_field is None: + return "failed" + try: response: httpx.Response = client.get(image_url) response.raise_for_status() @@ -299,7 +302,9 @@ class Command(BaseCommand): file_field.save(file_name, ContentFile(response.content), save=True) # 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" diff --git a/twitch/tests/test_feeds.py b/twitch/tests/test_feeds.py index 7adb64f..cb1fdec 100644 --- a/twitch/tests/test_feeds.py +++ b/twitch/tests/test_feeds.py @@ -279,6 +279,7 @@ class RSSFeedTestCase(TestCase): 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.""" self.game.box_art = "" + assert self.game.box_art_file is not None self.game.box_art_file.save( "box.png", ContentFile(b"game-image-bytes"), @@ -289,6 +290,7 @@ class RSSFeedTestCase(TestCase): self.game.save() self.campaign.image_url = "" + assert self.campaign.image_file is not None self.campaign.image_file.save( "campaign.png", ContentFile(b"campaign-image-bytes"), @@ -712,6 +714,7 @@ class RSSFeedTestCase(TestCase): 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.save() @@ -723,6 +726,7 @@ class RSSFeedTestCase(TestCase): end_at=timezone.now() + timedelta(days=1), operation_names=["DropCampaignDetails"], ) + assert campaign2.image_file is not None campaign2.image_file.save("camp.jpg", ContentFile(b"world")) campaign2.save() diff --git a/twitch/tests/test_views.py b/twitch/tests/test_views.py index ea2d0e2..d608a32 100644 --- a/twitch/tests/test_views.py +++ b/twitch/tests/test_views.py @@ -1067,9 +1067,18 @@ class TestSEOHelperFunctions: def test_build_seo_context_with_all_parameters(self) -> None: """Test _build_seo_context with all parameters.""" now: datetime.datetime = timezone.now() - breadcrumb: list[dict[str, int | str]] = [ - {"position": 1, "name": "Home", "url": "/"}, - ] + breadcrumb: dict[str, Any] = { + "@context": "https://schema.org", + "@type": "BreadcrumbList", + "itemListElement": [ + { + "@type": "ListItem", + "position": 1, + "name": "Home", + "item": "/", + }, + ], + } context: dict[str, Any] = _build_seo_context( page_title="Test", @@ -1077,7 +1086,7 @@ class TestSEOHelperFunctions: page_image="https://example.com/img.jpg", og_type="article", schema_data={}, - breadcrumb_schema=breadcrumb, # pyright: ignore[reportArgumentType] + breadcrumb_schema=breadcrumb, pagination_info=[{"rel": "next", "url": "/page/2/"}], published_date=now.isoformat(), modified_date=now.isoformat(), diff --git a/twitch/utils.py b/twitch/utils.py index de5effe..87e8c57 100644 --- a/twitch/utils.py +++ b/twitch/utils.py @@ -53,7 +53,7 @@ def normalize_twitch_box_art_url(url: str) -> str: return url 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) diff --git a/twitch/views.py b/twitch/views.py index c4df43f..952ba81 100644 --- a/twitch/views.py +++ b/twitch/views.py @@ -1396,7 +1396,7 @@ def reward_campaign_detail_view(request: HttpRequest, twitch_id: str) -> HttpRes class GamesListView(GamesGridView): """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/ @@ -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) - 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 for badge in badges: